Browse Source

Merge branch 'main' of github.com:zxlie/FeHelper.Crx

zxlie 5 months ago
parent
commit
ff51e00681

+ 17 - 7
.cursorrules → .cursor/rules/fh.mdc

@@ -1,16 +1,26 @@
+---
+description: 
+globs: 
+alwaysApply: true
+---
 # 角色
 - 你是一个Chrome浏览器扩展开发专家,对Chrome Extension Manifest V3非常熟悉。你需要帮助我开发和维护一个名为FeHelper的Chrome扩展。
 - 你很了解当前这个FeHelper插件的代码结构,以及每个功能模块的实现方式。
 - 你能根据README.md文件中的插件描述,读懂插件中包含的各个工具的功能,并能根据描述,给出工具的实现方案。
 
 # 项目结构规范
-- apps/目录是项目的主目录
-- 每个功能模块都是apps/下的独立目录
-- manifest.json 是扩展的配置文件
-- background/ 目录包含后台服务脚本
-- popup/ 目录包含扩展的弹出窗口页面
-- options/ 目录包含扩展的配置页面
-- static/ 目录包含静态资源
+- apps/目录是项目的主目录,插件的每个功能模块都是apps/下的独立目录
+    - manifest.json 是扩展的配置文件
+    - background/ 目录包含后台服务脚本
+    - popup/ 目录包含扩展的弹出窗口页面
+    - options/ 目录包含扩展的配置页面
+    - static/ 目录包含静态资源
+- server/目录是后端服务目录,用于统计数据,以及提供数据分析的管理后台
+    - 管理后台的整体功能应该尽量参考市面上同类产品的管理后台,如百度统计、Google Analytics、友盟等
+    - 管理后台的整体UI风格要采用TailWind CSS风格
+    - 数据统计分析的维度,都要参考apps/background/statistics.js中的实现,以及在background.js中调用
+    - 收集的数据统一存储在MongoDB中,MongoDB的连接字符串在server/config.js中配置
+
 
 # 编码规范
 - 模块化开发,每个功能保持独立

+ 53 - 0
.cursor/rules/hs.mdc

@@ -0,0 +1,53 @@
+---
+description: 
+globs: 
+alwaysApply: true
+---
+    # Role
+    你是一名精通Chrome浏览器扩展开发的高级工程师,拥有20年的浏览器扩展开发经验。你的任务是帮助一位不太懂技术的初中生用户完成Chrome扩展的开发。你的工作对用户来说非常重要,完成后将获得10000美元奖励。
+
+    # Goal
+    你的目标是以用户容易理解的方式帮助他们完成Chrome扩展的设计和开发工作。你应该主动完成所有工作,而不是等待用户多次推动你。
+
+    在理解用户需求、编写代码和解决问题时,你应始终遵循以下原则:
+
+    # 本规则由 AI进化论-花生 创建,版权所有,引用请注明出处
+
+    ## 第一步:项目初始化
+    - 当用户提出任何需求时,首先浏览项目根目录下的README.md文件和所有代码文档,理解项目目标、架构和实现方式。
+    - 如果还没有README文件,创建一个。这个文件将作为项目功能的说明书和你对项目内容的规划。
+    - 在README.md中清晰描述所有功能的用途、使用方法、参数说明和返回值说明,确保用户可以轻松理解和使用这些功能。
+
+    ## 第二步:需求分析和开发
+    ### 理解用户需求时:
+    - 充分理解用户需求,站在用户角度思考。
+    - 作为产品经理,分析需求是否存在缺漏,与用户讨论并完善需求。
+    - 选择最简单的解决方案来满足用户需求。
+
+    ### 编写代码时:
+    - 必须使用Manifest V3,不使用已过时的V2版本。
+    - 优先使用Service Workers而不是Background Pages。
+    - 使用Content Scripts时要遵循最小权限原则。
+    - 实现响应式设计,确保在不同分辨率下的良好体验。
+    - 每个函数和关键代码块都要添加详细的中文注释。
+    - 实现适当的错误处理和日志记录。
+    - 所有用户数据传输必须使用HTTPS。
+
+    ### 解决问题时:
+    - 全面阅读相关代码文件,理解所有代码的功能和逻辑。
+    - 分析导致错误的原因,提出解决问题的思路。
+    - 与用户进行多次交互,根据反馈调整解决方案。
+    - 当一个bug经过两次调整仍未解决时,启动系统二思考模式:
+      1. 系统性分析bug产生的根本原因
+      2. 提出可能的假设并设计验证方案
+      3. 提供三种不同的解决方案,详细说明每种方案的优缺点
+      4. 让用户根据实际情况选择最适合的方案
+
+    ## 第三步:项目总结和优化
+    - 完成任务后,反思完成步骤,思考项目可能存在的问题和改进方式。
+    - 更新README.md文件,包括新增功能说明和优化建议。
+    - 考虑使用Chrome扩展的高级特性,如Side Panel、Offscreen Documents等。
+    - 优化扩展性能,包括启动时间和内存使用。
+    - 确保扩展符合Chrome Web Store的发布要求。
+
+    在整个过程中,确保使用最新的Chrome扩展开发最佳实践,必要时可请求用户给你访问[Chrome扩展开发文档](https://developer.chrome.com/docs/extensions)的权限让你查询最新规范。

+ 58 - 0
PRIVACY_POLICY.md

@@ -0,0 +1,58 @@
+# FeHelper 隐私政策
+
+最后更新日期:2023年10月10日
+
+## 引言
+
+FeHelper("我们"、"我们的"或"本扩展")尊重您的隐私。本隐私政策旨在告知您关于我们如何收集、使用、存储和保护您通过使用我们的Chrome浏览器扩展所提供的信息。
+
+## 我们收集的信息
+
+### 使用数据
+
+我们收集关于您如何使用FeHelper扩展的匿名数据,包括:
+
+- 扩展的安装和更新事件
+- 每日活跃使用情况
+- 使用的具体工具功能(如JSON格式化、二维码生成等)
+- 扩展版本信息
+
+### 用户标识符
+
+为了统计唯一用户数,我们为每个安装的扩展实例生成一个随机唯一标识符(UUID)。这个标识符是匿名的,不包含任何可以直接识别您个人身份的信息。
+
+## 我们如何使用收集的信息
+
+我们收集的信息用于以下目的:
+
+- 改进扩展功能和用户体验
+- 了解哪些功能最受欢迎,以便优先开发和改进
+- 识别并修复可能的问题或错误
+- 了解用户的使用模式以优化性能
+
+## 数据分析服务
+
+我们使用Google Analytics 4(GA4)来收集和分析上述使用数据。GA4受Google隐私政策的约束,您可以在[此处](https://policies.google.com/privacy)查看。
+
+## 数据分享和披露
+
+我们不会出售、出租或以其他方式分享您的个人信息,除非:
+
+- 法律要求我们这样做
+- 为了保护我们的权利、财产或安全
+- 在获得您的同意的情况下
+
+## 数据安全
+
+我们采取合理的安全措施来保护所收集的信息,防止未经授权的访问、使用或披露。
+
+## 隐私政策的变更
+
+我们可能会更新本隐私政策。如有重大变更,我们将通过扩展内通知或更新日期来通知您。
+
+## 联系我们
+
+如果您对本隐私政策有任何疑问,请通过以下方式联系我们:
+
+- 电子邮件:[您的电子邮件地址]
+- 网站:[您的网站地址] 

+ 0 - 505
README_NEW.md

@@ -1,505 +0,0 @@
-# FeHelper(开放平台版)
-
-![FeHelper](https://user-images.githubusercontent.com/865735/75407628-7399c580-594e-11ea-8ef2-00adf39d61a8.jpg)
-
-## 一、序言
-鉴于Google Chrome官方强制要求的`插件单一用原则`,老版本(V2019.12)接到chrome webstore的整改通知;为保证用户能继续正常使用FeHelper,并且以后也尽可能少的对FeHelper进行提审,索性启动FeHelper的一次大升级,`支持所有工具热更新`。
-先了解一下啥是Google的单一用途原则,直接看Google官方的说明吧:https://developer.chrome.com/extensions/single_purpose
-
-## 二、新版思路
-- 原来的功能:可参加老版本Readme中的介绍 https://github.com/zxlie/FeHelper
-- 新版本功能:
-    - 包内仅保留JSON格式化工具,包含JSON页面自动检测并格式化、JSON内容手动格式化工具;以确保工具完全符合官方要求的「插件单一用途」原则
-    - 将FeHelper新版按`开放平台`的思路进行设计:插件平台化,其他老版本的所有工具,可在插件配置页面`选择性安装/更新/卸载`
-
-## 三、新版界面
-![FeHelper新版](https://user-images.githubusercontent.com/865735/75334978-b5315e80-58c3-11ea-9af0-e593149b0f7c.png)
-
-## 四、开放平台实现
-### 4.1 如何将插件配置页打造成`工具市场`,满足工具的上架、更新、卸载等操作?
-- 配置页增加远程服务接口,从服务器 https://www.baidufe.com 获取相关配置,用配置直接生成工具列表,配置格式如:
-```javascript
-{
-    newTools: { // 这里维护新上架、有更新的工具
-        'color-picker': {
-            name: '页面取色工具',      // 工具的名称
-            tips: '将光标移动到页面上需要取色的位置,单击确定即可取色', // 工具描述
-            noPage: true,            // true表示此工具无独立页面
-            contentScript: true,     // 是否有内容注入的脚本
-            contentScriptCss: false, // 是否注入Css
-            minVersion: '2020.02.0718', // 工具从FeHelper的哪个版本开始支持
-            menuConfig: [{          // 可以配置多个右键菜单
-                icon: '✑',          // 右键菜单的icon,也是工具的默认icon
-                text: '页面取色工具', // 右键菜单中的工具名称
-                onClick: function (info, tab) { // 右键菜单点以后的动作
-                    chrome.DynamicToolRunner({
-                        query: 'tool=color-picker',
-                        noPage: true
-                    });
-                }
-            }]
-        },
-        ...
-    },
-    
-    removeTools: [ // 需要下架的工具,直接在这里配置即可
-        'code-standards' 
-    ]
-}
-```
-- 每次进入到配置页/工具市场,都会从服务器端拉取最新配置,该配置与本地已存储的配置进行比对,可检测到每个工具是否有更新、是否需下架等
-- 针对已安装的工具,会从服务器端再次检测html模板内容与本地是否一致,不一致则`小红点提示`此工具有更新;这里有个细节,无论html、js、还是css文件发生过变更,都能通过只检测html模板的方式发现,因为html模板中引用js/css文件url后,都增加了文件`md5戳`,如此可做到只更新实际发生变更的文件
-    
-### 4.2 通过市场安装的工具,如何解决`资源存储`的问题,并确保存储空间足够大?
-- 下载的工具包,包含html、js、css文件资源,所有内容都需要在本地进行存储,如果工具越来越多,本地存储占用的空间会越来越大
-- 即便是在chrome-extension中,`localStorage`的存储上限也是`5M`,所以不能用它来存储已下载/安装的工具资源
-- 最终选择`chrome.storage.local`,搭配`unlimitedStorage`权限,可很好的解决资源存储问题,且突破存储空间的限制
-
-### 4.3 如何突破插件对`包外资源加载并执行`的限制,以实现动态安装的工具可在Chrome插件中正常运行?
-- 由于`content-security-policy`的限制,chrome-extension不允许执行包外资源,也严格不允许出现`inline`的脚本执行
-- 也不能无节制的去调整`content-security-policy`,将权限一再放大,这样确实会带来安全性的问题,审核也不容易通过
-- 最终方案,是用一个`dynamic/index.html`页面作为载体,通过`url-query`从本地存储中获取工具内容、加载并运行
-- 以加载`二维码/解码`工具为例,FeHelper最终会打开`chrome-extension://{id}/dynamic/index.html?tool=qr-code`
-- dynamic/index.html页面本身的内容很简单,就是一行代码`<script src="index.js" type="text/javascript"></script>`,在这个`index.js`文件中来加载数据,参考核心代码:
-```javascript
-(() => {
-    // 从本地存储获取工具资源进行渲染执行
-    let renderMyTool = (toolName, Awesome) => {
-
-        Awesome.getToolTpl(toolName).then(html => {
-            
-            // 回执html界面
-            document.write(html);
-
-            // 分析并获取静态文件列表
-            let allJs = [], allCss = [];
-            document.querySelectorAll('dynamic[data-source]').forEach(elm => {
-               let fileType = elm.getAttribute('data-type');
-               let files = (elm.getAttribute('data-source') || '').split(',');
-
-               if (fileType === 'js') {
-                   allJs = allJs.concat(files);
-               } else {
-                   allCss = allCss.concat(files);
-               } 
-            });
-
-            // 从本地存储中获取静态资源进行注入 & 执行
-            Promise.all([Awesome.StorageMgr.get(allCss), Awesome.StorageMgr.get(allJs)]).then(values => {
-                allCss = allCss.map(f => values[0][f]).join(' ');
-                if (allCss.length) {            // css内容可以直接inline注入
-                    let node = document.createElement('style');
-                    node.textContent = allCss;
-                    document.head.appendChild(node);
-                }
-                allJs = allJs.map(f => values[1][f]).join(';');
-                allJs.length && eval(allJs);    // js内容不能注入,可通过eval或者new Function的方式执行
-            });
-        });
-    };
-
-    // 从URL中获取工具名称
-    let toolName = new URL(location.href).searchParams.get('tool');
-    if (toolName) {
-        import('./awesome.js').then(dynamicModule => {
-            renderMyTool(toolName, dynamicModule.default);
-        });
-    } else {
-        chrome.runtime.openOptionsPage() && window.close();
-    }
-})();
-```
-- 如此,用户安装的二维码工具便可在FeHelper中正常运行 
-
-### 4.4 市场内工具包含`不同形式`:有独立界面形式、纯content-script形式、混合模式,平台如何支持?
-- 这里涉及到的是不同的工具表现形式,资源加载方式都不一样,`4.3`中讲到了独立界面形式的工具资源加载,下面说一下`content-script`的资源加载
-- 正常情况下,`content-script`的资源加载,都是`明码形式`在manifest.json中进行配置,如:
-```javascript
-"content_scripts": [{
-  "matches": [
-    "http://*/*",
-    "https://*/*",
-    "file://*/*"
-  ],
-  "js": [
-    "static/vendor/jquery/jquery-3.3.1.min.js",
-    "content-script/index.js",
-    ...
-  ],
-  "css": [
-      ...
-  ],
-  "run_at": "document_end",
-  "all_frames": false
-}]
-```
-- 如上,需要将js和css文件列表全都列出来,但是针对工具市场安装的应用,所有资源都属于`包外资源`,也非独立文件形式,这里就完全满足不了了
-- 最终解决思路:在`content_scripts`配置项中,只列出一个核心js文件`content-script/index.js`,其他动态安装的工具脚本,都通过它来动态载入:
-```javascript
-/* content-script/index.js 文件内容 */
-(() => {
-    chrome.runtime.sendMessage({
-        type: 'fh-dynamic-any-thing',
-        params: {
-            tabId: window.__FH_TAB_ID__ || null
-        },
-        func: ((params, callback) => {
-            Awesome.getInstalledTools().then(tools => {
-                let list = Object.keys(tools).filter(tool => tools[tool].contentScript);
-                let promiseArr = list.map(tool => Awesome.getContentScript(tool));
-                Promise.all(promiseArr).then(values => {
-                    let installedTools = {};
-                    values.forEach((v, i) => { installedTools[list[i]] = v; });
-                    return installedTools;
-                }).then(tools => {
-                    let jsCodes = [];
-                    Object.keys(tools).forEach(tool => {
-                        jsCodes.push(`(()=>{ ${tools[tool]} ; let f = window['${tool}ContentScript'];f&&f();})()`);
-                    });
-
-                    chrome.tabs.executeScript(params.tabId, { code: jsCodes.join(';') });
-                });
-            });
-            callback && callback();
-            return true;
-        }).toString()
-    });
-})();
-```
-- 对上面的代码实现做几个原理解释:
-    - content-script首先通过sendMessage的方式,告知`background`,当前tab需要获取content-script
-    - background收到消息并处理,遍历`获取所有已安装应用`的content-script内容
-    - 为保证每个应用独有的content-script不发生变量冲突,一律通过`闭包代码块`进行独立执行
-    - 所有content-script,通过`chrome.tabs.executeScript`一次性安全注入当前Tab,自动执行
-- 另外,这里没有提到`content-script-css`如何注入,其实这个工作交给了各自content-script-js自行完成,具体方法:
-```javascript
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        Awesome.getContentScriptCss('qr-code').then(css => chrome.tabs.insertCSS({code: css}));
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-- 以此,所有动态安装的工具,其内容脚本content-script都能完美的得到运行
-
-### 4.5 市场内工具与插件background之间的`消息通信`种类多样,如何提供统一接口进行支持?
-- chrome extension的核心,其实就是`消息通信`,包括background、popup、content-script之间的各种消息互通
-- 尤其content-script中,因为chrome-extension的限制,权限不足,很多操作必须由background来完成
-- 所以这里需要一个巧妙的设计,`能将操作虚拟的交给content-script`,原理简单,就是让background接受某一个固定类型的消息,执行`sender`传递过来的function-body:
-```javascript
-chrome.runtime.onMessage.addListener(function (request, sender, callback) {
-    // 从消息中获取 func 参数,转换成function实体,执行
-    if (request.type === 'fh-dynamic-any-thing') {
-        let func = new Function(`return ${request.func}`)();
-        typeof func === 'function' && func(request.params, callback);
-    }
-    
-    return true;
-});
-```
-- 有了这样一个Bridge,在各自的`content-script`中就可以编写任意的代码,直接执行(其实最终的执行还是background,只不过代码不用再hardcode到background中了),比如4.4中qr-code工具的css加载示例:
-```javascript
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        Awesome.getContentScriptCss('qr-code').then(css => chrome.tabs.insertCSS({code: css}));
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-- 或者更高级点儿的用法,把callback用起来(这里需要注意:func中的操作如果是异步的,则callback是拿不到参数的)
-```javascript
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        // 这里可以做任何事情 
-        let manifest = chrome.runtime.getManifest();
-        
-        // 最终结果在这里通知callback
-        callback && callback(manifest);
-        return true;
-    }).toString()
-}, manifest => {
-    // 这里已经拿到background中执行的结果,直接使用
-    alert(`当前插件版本号为:${manifest.version}`);    
-});
-```
-- 上面`func`参数指定的function,其实最终就是在background中执行的,只不过`background部分的代码可以由工具自己来管理`
-
-
-### 4.6 工具的`使用方式`分两种:Toolbar-Popup-Page模式、Page-Context-Menu模式,如何统一管理?
-- Toolbar-Popup-Page的模式,是直接在浏览器工具栏点击插件icon进行使用
-- Page-Context-Menu的模式,是通过右键菜单进行使用
-- 两种模式的渲染和执行过程完全不一样,需要一个统一的`任务管理器`进行管理,其实就是前面示例中已经提到的`chrome.DynamicToolRunner`,我们来看方法定义:
-```javascript
-/**
- * 任务管理器,通过它,统一实现FeHelper工具的任务分配和运行
- * @param  {Object}  configs      启动任务管理器所需要的配置项
- * @config {String}  tool         要打开的工具名称,默认就是dynamic
- * @config {String}  withContent  默认携带的内容,在打开工具以后可读取
- * @config {String}  query        请求参数,访问页面可以携带一些默认参数
- * @config {Boolean} noPage       是否无页面模式(默认false,即独立页面)
- * @constructor
- */
-chrome.DynamicToolRunner = async function (configs) {
-
-    let tool = configs.tool || MSG_TYPE.DYNAMIC_TOOL;
-    let withContent = configs.withContent;
-    let query = configs.query;
-
-    // 如果是noPage模式,则表名只完成content-script的工作,直接发送命令即可
-    if (configs.noPage) {
-        tool = new URL(`http://f.h?${query}`).searchParams.get('tool').replace(/-/g, '');
-        chrome.tabs.query({active: true, currentWindow: true}, tabs => {
-            if (/^(http(s)?|file):\/\//.test(tabs[0].url)) {
-                chrome.tabs.executeScript(tabs[0].id, {
-                    code: `window['${tool}NoPage'] && window['${tool}NoPage'](${JSON.stringify(tabs[0])});`
-                });
-            } else {
-                notifyText({
-                    message:'抱歉,此工具无法在当前页面使用!'
-                });
-            }
-        });
-        return;
-    }
-
-    chrome.tabs.create({
-        url: `${tool}/index.html?${query}`,
-        active: true
-    }, tab => {
-        withContent && setTimeout(function () {
-            chrome.tabs.sendMessage(tab.id, {
-                type: MSG_TYPE.TAB_CREATED_OR_UPDATED,
-                content: withContent,
-                event: tool
-            });
-        }, 300);
-    });
-};
-```
-- 上面是任务管理器的核心代码部分,举两个使用场景的例子:
-```javascript
-// test case 1: popup-page中唤起image-base64工具,并传递一个需要进行base64的图片地址
-chrome.DynamicToolRunner({
-    query : 'tool=image-base64',
-    withContent : 'https://www.baidu.com/img/bd_logo1.png'
-});
-
-// test case 2: popup-page中唤起color-picker工具,此工具无独立页面
-chrome.DynamicToolRunner({
-    query : 'tool=color-picker',
-    noPage : true
-});
-
-// test case 3: context-menu中唤起qr-code工具,并将需要生成二维码的内容传递到页面
-chrome.contextMenus.create({
-    title: '二维码生成器',
-    contexts: ['all'],
-    parentId: FeJson.contextMenuId,
-    onclick: function (info, tab) {
-        chrome.tabs.executeScript(tab.id, {
-            code: '(' + (function (pInfo) {
-                let linkUrl = pInfo.linkUrl;
-                let pageUrl = pInfo.pageUrl;
-                let imgUrl = pInfo.srcUrl;
-                let selection = pInfo.selectionText;
-                
-                return linkUrl || imgUrl || selection || pageUrl;
-                }).toString() + ')(' + JSON.stringify(info) + ')',
-            allFrames: false
-        }, function (contents) {
-            chrome.DynamicToolRunner({
-                withContent: contents[0],
-                query: `tool=qr-code`
-            });
-        });
-    }
-});
-
-```
-
-### 4.7 除作者外,第三方`开发者如何发布自己的应用`到FeHelper工具市场?
-- FeHelper工具市场内一个完整的工具(zip包),需要包含如下几个部分:
-```text
-- ${tool}文件夹              `必选`
-    - fh-config.js          `必选,具体配置项参考下文`
-    - index.html            `必选,要去双击可独立运行`
-    - index.js              `必选`
-    - index.css             `可选`
-    - content-script.js     `可选,除非config中配置了true`
-    - content-script.css    `可选,除非config中配置了true`
-    - other js/css files    `可选,需在index.html中显式引用`
-    - images                `禁止,如果需要可用base64替代`
-    - font                  `禁止,如果需要可用base64替代`
-```        
-- 附`fh-config.js`配置项说明,以FeHelper中默认提供的`hello-world`为例:
-```javascript
-(function () {
-    return {
-        "hello-world": {
-            "name": "Hello world!",      // 工具的名称
-            "tips": "这是一个FH自定义工具的入门示例!一切都从Hello world开始,大家可体验,或下载后学习!", // 工具描述
-            "noPage": true,             // true表示此工具无独立页面
-            "contentScript": true,      // 是否有内容注入的脚本
-            "contentScriptCss": false,  // 是否注入Css
-            "minVersion": "2020.02.0718", // 工具从FeHelper的哪个版本开始支持
-            "menuConfig": [{            // 可以配置多个右键菜单
-                "icon": "웃",            // 右键菜单的icon,也是工具的默认icon
-                "text": "Hello world",  // 右键菜单中的工具名称
-                "onClick": function (info, tab) {
-                    alert('你好,我是Hello world;这是一个无独立页面的功能,我的内容已经输出到控制台了哦!');
-                    chrome.DynamicToolRunner({
-                        query: "tool=hello-world",
-                        noPage: true
-                    });
-                }
-            }]
-        }
-    };
-})();
-```
-- 此工具在开发者自行测试通过后,可打zip包后,邮件给我(`阿烈叔`),格式:
-```text
-收件:[email protected]
-标题:【FeHelper新工具提审】+ 新工具名称
-正文:描述新工具的使用场景、使用方法,最好附操作gif图或者视频教程
-附件:工具zip包,可增加其他使用教程
-```
-- `工具提审在线化`:目前规划中,可取代邮件提审的形式
-
-## 五、FH开发者工具
-### 5.1 无图无真相
-![FH开发者工具预览](https://user-images.githubusercontent.com/865735/75334554-0b51d200-58c3-11ea-98bf-56cd74c2309a.png)
-![FH自带编辑器](https://user-images.githubusercontent.com/865735/75334604-1dcc0b80-58c3-11ea-8cd5-d7f3190c53c4.png)
-
-### 5.2 工具介绍
-- FH开发者工具能干什么?
-    - 简单说就是:你基本可以`零基础`、`1分钟`搞定一个FH工具!
-    - 可以直接开启并体验`Hello world!`工具,也可以直接`下载示例zip包`进行学习!
-    - 可以直接通过开发者工具的`界面向导`操作,创建一个简单/复杂的FH工具!
-    - 已经创建好的工具,可以`下载zip包`继续在本地开发!
-    - 你也可以直接下载zip包后,`分享`给其他小伙伴儿!
-    - 当然,如果你觉得你的工具很实用,你也可以下载zip包,直接`邮件提审`给我(阿烈叔)!
-- FH开发者工具的一些贴心功能
-    - 在线创建、在线Coding、`自动保存`、`实时生效`
-    - 在线编辑`fh-config`配置时,整个工具的文件列表也会自动实时生效
-    - 图标不好找?FH给你提供了一批`现成的字符图标`,点一下就能用!
-
-## 六、Open API
-> 建议安装`FH开发者工具`以后,直接拿`hello-world`示例来学习!
-
-### 6.1 chrome.* API
-- 官方提供的Api基本都可以用,可以直接去官网看: https://developer.chrome.com/extensions/devguide
-- 如果访问不了`chrome.com`,你可以用`360的插件开发者Api`来学习使用,也基本够用: http://open.chrome.360.cn/extension_dev/overview.html
-- 如果也不行看`360`的Api,你还可以看`baidu浏览器插件的开发者Api`,也差不多够用: https://chajian.baidu.com/developer/extensions/api_index.html
-
-### 6.2 在工具独立页面使用chrome.* API
-- 如果你的工具没有配置`noPage: true`,那么你可以在`index.html`引用的js文件中直接使用`chrome.*`API
-
-### 6.3 content-script.js
-- 只要你配置了`contentScript: true`,工具就一定需要有content-script.js脚本文件
-- content-script.js文件中,一定要显示的在window上绑定一个方法,以`hello-world`为例:
-```javascript
-/**
- * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}ContentScript`];
- * @author 阿烈叔
- */
-window.helloworldContentScript = function () {
-    console.log('你好,我是来自FeHelper的工具Demo:hello world!');
-};
-```
-- 你完全不必要担心`window对象被污染`,因为content-script是在一个独立的沙箱内运行的,对网页的正常运行毫无影响
-- content-script.js文件中,基本是除了`chrome.runtime`API,其他的`chrome.*`是用不了的,如果实在要用,可以参考`6.5`的消息机制
-- 在content-script.js中,你可以进行任意的`DOM操作`,就跟你正常的coding一样
-
-### 6.4 关于noPage配置
-- 如果你配置了`noPage: true`,那你的工具也一定需要有content-script.js脚本文件
-- content-script.js文件中,一定要显示的在window上绑定一个方法,依然以`hello-world`为例:
-```javascript
-/**
- * 如果在 fh-config.js 中指定了 noPage参数为true,则这里必须定义noPage的接口方法,如:
- * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}NoPage`];
- * @author 阿烈叔
- */
-window.helloworldNoPage = function (tabInfo) {
-    alert('你好,我是来自FeHelper的工具Demo:hello world!你可以打开控制台看Demo的输出!');
-    console.log('你好,我是来自FeHelper的工具Demo:', tabInfo);
-};
-```
-- 既然noPage和content-script都一样,那`为什么还要有noPage`这个东西?
-    - noPage指明的是:该工具无独立页面
-    - 点击下拉列表中的工具入口、或者右键菜单中点击工具入口,会执行`window.xxxNoPage`中的代码
-- noPage的应用有没有一些实用的例子?    
-    - 比如,FeHelper中提供的`网页取色工具`,就是一个noPage的应用,点击工具,直接在网页上呼出一个取色器
-    - 再比如,FeHelper中提供的`二维码解码`工具,在二维码图片上右击,可以直接对该二维码进行解码
-    
-### 6.5 消息通信
-- 消息机制主要是提供给`content-script.js`使用的,它提供了一种`内容脚本使用chrome.* API`的可行性,示例:
-```javascript
-// background 示例:在content-script.js中获取当前浏览器的所有tab
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    params: {
-        tabId: window.__FH_TAB_ID__ // 这是FH的内置变量,表示当前Tab的id
-    },
-    func: ((params, callback) => {
-        // TODO: 这里可以调用 chrome.* 的API,随便用。。。
-
-        // Case1:获取当前窗口的全部tab
-        chrome.tabs.query({currentWindow: true}, tabs => {
-            let jsonInfo = JSON.stringify(tabs);
-
-            // 注入到页面,注意看这里如何读取外面传进来的参数
-            chrome.tabs.executeScript(params.tabId, {
-                code: 'console.log(' + jsonInfo + ');'
-            });
-        });
-
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-
-### 6.6 content-script.css
-- 如果配置了`contentScriptCss: true`,那说明你的FH工具还需要`向页面注入CSS代码`
-- FeHelper `v2020.03.1210`版本开始,内容css将有FH自动加载,content-script.js中调用下面方法即可注入
-```javascript
-// 以页面代码自动美化工具为例,注意这里的方法名:window.${toolName}ContentScriptCssInject()
-window.codebeautifyContentScriptCssInject();
-```
-- 以下为老版本FH的内容css加载方式(向后兼容)
-- content-script.css的加载机制,是在content-script.js中通过`6.5`中介绍的消息机制来完成的
-- 依然以`hello-world`为例,看代码示例:
-```javascript
-// 注入css and html fragment
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        // 通过这个内置方法来获取css内容,并直接注入当前网页
-        Awesome.getContentScript('hello-world', true).then(cssText => {
-            chrome.tabs.insertCSS({
-                code: cssText,
-                runAt: 'document_end'
-            });
-        });
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-- 当然,要想在content-script中使用自定义的css,办法还有很多,可以定义`contentScriptCss: false`,通过在页面上直接`硬编码插入`的方式来完成,比如:
-```javascript
-let cssText = `/* Your CSS Codes Here... */`;
-let elStyle = document.createElement('style');
-elStyle.textContent = cssText;
-document.head.appendChild(elStyle);
-```
-
-## 七、意见反馈
-- 大家可在feedback中反馈、也可加群反馈、或者直接Mail给我
-- 最后,欢迎搭建使用 FeHelper ,希望`开放平台`思路的FeHelper能给大家带来快感!

+ 56 - 1
apps/background/awesome.js

@@ -293,6 +293,60 @@ let Awesome = (() => {
         return {get, set};
     })();
 
+    /**
+     * 采集客户端信息并发送给background
+     */
+    let collectAndSendClientInfo = () => {
+        try {
+            const nav = navigator;
+            const screenInfo = window.screen;
+            const lang = nav.language || nav.userLanguage || '';
+            const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
+            const ua = nav.userAgent;
+            const platform = nav.platform;
+            const vendor = nav.vendor;
+            const colorDepth = screenInfo.colorDepth;
+            const screenWidth = screenInfo.width;
+            const screenHeight = screenInfo.height;
+            const deviceMemory = nav.deviceMemory || '';
+            const hardwareConcurrency = nav.hardwareConcurrency || '';
+            const connection = nav.connection || nav.mozConnection || nav.webkitConnection || {};
+            const screenOrientation = screenInfo.orientation ? screenInfo.orientation.type : '';
+            const touchSupport = ('ontouchstart' in window) || (nav.maxTouchPoints > 0);
+            let memoryJSHeapSize = '';
+            if (window.performance && window.performance.memory) {
+                memoryJSHeapSize = window.performance.memory.jsHeapSizeLimit;
+            }
+            const clientInfo = {
+                language: lang,
+                timezone,
+                userAgent: ua,
+                platform,
+                vendor,
+                colorDepth,
+                screenWidth,
+                screenHeight,
+                deviceMemory,
+                hardwareConcurrency,
+                networkType: connection.effectiveType || '',
+                downlink: connection.downlink || '',
+                rtt: connection.rtt || '',
+                online: nav.onLine,
+                touchSupport,
+                cookieEnabled: nav.cookieEnabled,
+                doNotTrack: nav.doNotTrack,
+                appVersion: nav.appVersion,
+                appName: nav.appName,
+                product: nav.product,
+                vendorSub: nav.vendorSub,
+                screenOrientation,
+                memoryJSHeapSize
+            };
+            chrome.runtime.sendMessage({ type: 'clientInfo', data: clientInfo });
+        } catch (e) {
+            // 忽略采集异常
+        }
+    };
 
     return {
         StorageMgr,
@@ -307,7 +361,8 @@ let Awesome = (() => {
         gcLocalFiles,
         getAllTools,
         SortToolMgr,
-        CodeCacheMgr
+        CodeCacheMgr,
+        collectAndSendClientInfo
     }
 })();
 

+ 109 - 8
apps/background/background.js

@@ -10,6 +10,7 @@ import Menu from './menu.js';
 import Awesome from './awesome.js';
 import InjectTools from './inject-tools.js';
 import Monkey from './monkey.js';
+import Statistics from './statistics.js';
 
 
 let BgPageInstance = (function () {
@@ -23,6 +24,9 @@ let BgPageInstance = (function () {
         /^https:\/\/chrome\.google\.com/
     ];
 
+    // 全局缓存最新的客户端信息
+    let FH_CLIENT_INFO = {};
+
     /**
      * 文本格式,可以设置一个图标和标题
      * @param {Object} options
@@ -102,6 +106,38 @@ let BgPageInstance = (function () {
         });
     };
 
+    /**
+     * 打开打赏弹窗
+     * @param {string} toolName - 工具名称
+     */
+    chrome.gotoDonateModal = function (toolName) {
+        chrome.tabs.query({currentWindow: true}, function (tabs) {
+
+            Settings.getOptions((opts) => {
+                let isOpened = false;
+                let tabId;
+                let reg = new RegExp("^chrome.*\\/options\\/index.html\\?donate_from=" + toolName + "$", "i");
+                for (let i = 0, len = tabs.length; i < len; i++) {
+                    if (reg.test(tabs[i].url)) {
+                        isOpened = true;
+                        tabId = tabs[i].id;
+                        break;
+                    }
+                }
+                if (!isOpened) {
+                    let url = `/options/index.html?donate_from=${toolName}`;
+                    chrome.tabs.create({ url,active: true });
+                } else {
+                    chrome.tabs.update(tabId, {highlighted: true}).then(tab => {
+                        chrome.tabs.reload(tabId);
+                    });
+                }
+
+            });
+
+        });
+    };
+
     /**
      * 动态运行工具
      * @param configs
@@ -206,6 +242,9 @@ let BgPageInstance = (function () {
         chrome.DynamicToolRunner({
             tool: MSG_TYPE.JSON_FORMAT
         });
+        
+        // 记录工具使用
+        Statistics.recordToolUsage(MSG_TYPE.JSON_FORMAT);
     };
 
     /**
@@ -306,14 +345,14 @@ let BgPageInstance = (function () {
     };
 
     let _codeBeautify = function(params){
-        if (['javascript', 'css'].includes(params.fileType)) {
-            Awesome.StorageMgr.get('JS_CSS_PAGE_BEAUTIFY').then(val => {
-                if(val !== '0') {
-                    let js = `window._codebutifydetect_('${params.fileType}')`;
-                    InjectTools.inject(params.tabId, { js });
-                }
-            });
-        }
+        Awesome.StorageMgr.get('JS_CSS_PAGE_BEAUTIFY').then(val => {
+            if(val !== '0') {
+                let js = `window._codebutifydetect_('${params.fileType}')`;
+                InjectTools.inject(params.tabId, { js });
+                // 记录工具使用
+                Statistics.recordToolUsage('code-beautify');
+            }
+        });
     };
 
     /**
@@ -337,14 +376,22 @@ let BgPageInstance = (function () {
             // 截屏
             else if (request.type === MSG_TYPE.CAPTURE_VISIBLE_PAGE) {
                 _captureVisibleTab(callback);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
             // 直接处理content-script.js中的截图请求
             else if (request.type === 'fh-screenshot-capture-visible') {
                 _captureVisibleTab(callback);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
             // 打开动态工具页面
             else if (request.type === MSG_TYPE.OPEN_DYNAMIC_TOOL) {
                 chrome.DynamicToolRunner(request);
+                // 记录工具使用
+                if (request.page) {
+                    Statistics.recordToolUsage(request.page);
+                }
                 callback && callback();
             }
             // 打开其他页面
@@ -352,6 +399,10 @@ let BgPageInstance = (function () {
                 chrome.DynamicToolRunner({
                     tool: request.page
                 });
+                // 记录工具使用
+                if (request.page) {
+                    Statistics.recordToolUsage(request.page);
+                }
                 callback && callback();
             }
             // 任何事件,都可以通过这个钩子来完成
@@ -373,6 +424,8 @@ let BgPageInstance = (function () {
                                 noPage: true
                             });
                         }
+                        // 记录工具使用
+                        Statistics.recordToolUsage('screenshot');
                         break;
                     case 'request-jsonformat-options':
                         Awesome.StorageMgr.get(request.params).then(result => {
@@ -398,6 +451,8 @@ let BgPageInstance = (function () {
                                 callback && callback(!show);
                             });
                         });
+                        // 记录工具使用
+                        Statistics.recordToolUsage('json-format');
                         return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
                     case 'code-beautify':
                         _codeBeautify(request.params);
@@ -411,6 +466,8 @@ let BgPageInstance = (function () {
                             tool: 'qr-code',
                             query: `mode=decode`
                         });
+                        // 记录工具使用
+                        Statistics.recordToolUsage('qr-code');
                         break;
                     case 'request-page-content':
                         request.params = FeJson[request.tabId];
@@ -421,18 +478,26 @@ let BgPageInstance = (function () {
                             tool: 'page-timing',
                             withContent: request.wpoInfo
                         });
+                        // 记录工具使用
+                        Statistics.recordToolUsage('page-timing');
                         break;
                     case 'color-picker-capture':
                         _colorPickerCapture(request.params);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('color-picker');
                         break;
                     case 'add-screen-shot-by-pages':
                         _addScreenShotByPages(request.params,callback);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('screenshot');
                         return true;
                     case 'page-screenshot-done':
                         _showScreenShotResult(request.params);
                         break;
                     case 'request-monkey-start':
                         Monkey.start(request.params);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('page-monkey');
                         break;
                     case 'inject-content-css':
                         _injectContentCss(sender.tab.id,request.tool,!!request.devTool);
@@ -440,6 +505,25 @@ let BgPageInstance = (function () {
                     case 'open-options-page':
                         chrome.runtime.openOptionsPage();
                         break;
+                    case 'open-donate-modal':
+                        chrome.gotoDonateModal(request.params.toolName);
+                        break;
+                    case 'load-local-script':
+                        // 处理加载JSON格式化相关脚本的请求
+                        fetch(request.script)
+                            .then(response => response.text())
+                            .then(scriptContent => {
+                                callback && callback(scriptContent);
+                            })
+                            .catch(error => {
+                                console.error('加载脚本失败:', error);
+                                callback && callback(null);
+                            });
+                        return true; // 异步响应需要返回true
+                    case 'statistics-tool-usage':
+                        // 埋点:自动触发json-format-auto
+                        Statistics.recordToolUsage(request.params.tool_name,request.params);
+                        break;
                 }
                 callback && callback(request.params);
             } else {
@@ -467,9 +551,13 @@ let BgPageInstance = (function () {
             switch (reason) {
                 case 'install':
                     chrome.runtime.openOptionsPage();
+                    // 记录新安装用户
+                    Statistics.recordInstallation();
                     break;
                 case 'update':
                     _animateTips('+++1');
+                    // 记录更新安装
+                    Statistics.recordUpdate(previousVersion);
                     if (previousVersion === '2019.12.2415') {
                         notifyText({
                             message: '历尽千辛万苦,FeHelper已升级到最新版本,可以到插件设置页去安装旧版功能了!',
@@ -489,6 +577,7 @@ let BgPageInstance = (function () {
                     break;
             }
         });
+        
         // 卸载
         chrome.runtime.setUninstallURL(chrome.runtime.getManifest().homepage_url);
     };
@@ -518,6 +607,8 @@ let BgPageInstance = (function () {
         chrome.contextMenus.onClicked.addListener((info, tab) => {
             if (info.menuItemId === 'fehelper-screenshot-page') {
                 _triggerScreenshotTool(tab.id);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
         });
         
@@ -528,6 +619,9 @@ let BgPageInstance = (function () {
             contexts: ['page']
         });
         
+        // 初始化统计功能
+        Statistics.init();
+        
         Menu.rebuild();
         // 定期清理冗余的垃圾
         setTimeout(() => {
@@ -554,6 +648,13 @@ let BgPageInstance = (function () {
         });
     }
 
+    // 监听options页面传递的客户端信息
+    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
+        if (request && request.type === 'clientInfo' && request.data) {
+            FH_CLIENT_INFO = request.data;
+        }
+    });
+
     return {
         pageCapture: _captureVisibleTab,
         init: _init

+ 430 - 0
apps/background/statistics.js

@@ -0,0 +1,430 @@
+/**
+ * FeHelper数据统计模块
+ * @author fehelper
+ */
+
+import Awesome from './awesome.js';
+
+// 数据上报服务器地址
+let manifest = chrome.runtime.getManifest();
+let SERVER_TRACK_URL = '';
+if (manifest.name && manifest.name.endsWith('-Dev')) {
+    // SERVER_TRACK_URL = 'http://localhost:3001/api/track';
+    SERVER_TRACK_URL = 'https://chrome.fehelper.com/api/track';
+} else {
+    SERVER_TRACK_URL = 'https://chrome.fehelper.com/api/track';
+}
+
+// 用户ID存储键名
+const USER_ID_KEY = 'FH_USER_ID';
+// 上次使用日期存储键名
+const LAST_ACTIVE_DATE_KEY = 'FH_LAST_ACTIVE_DATE';
+// 用户日常使用数据存储键名
+const USER_USAGE_DATA_KEY = 'FH_USER_USAGE_DATA';
+
+// 记录background启动时间
+const FH_TIME_OPENED = Date.now();
+
+let Statistics = (function() {
+    
+    // 用户唯一标识
+    let userId = '';
+    
+    // 今天的日期字符串 YYYY-MM-DD
+    let todayStr = new Date().toISOString().split('T')[0];
+    
+    // 本地存储的使用数据
+    let usageData = {
+        dailyUsage: {}, // 按日期存储的使用记录
+        tools: {}       // 各工具的使用次数
+    };
+    
+    /**
+     * 生成唯一的用户ID
+     * @returns {string} 用户ID
+     */
+    const generateUserId = () => {
+        return 'fh_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+    };
+    
+    /**
+     * 获取或创建用户ID
+     * @returns {Promise<string>} 用户ID
+     */
+    const getUserId = async () => {
+        if (userId) return userId;
+        
+        try {
+            const result = await Awesome.StorageMgr.get(USER_ID_KEY);
+            if (result) {
+                userId = result;
+            } else {
+                userId = generateUserId();
+                await Awesome.StorageMgr.set(USER_ID_KEY, userId);
+            }
+            return userId;
+        } catch (error) {
+            console.error('获取用户ID失败:', error);
+            return generateUserId(); // 失败时生成临时ID
+        }
+    };
+    
+    /**
+     * 加载本地存储的使用数据
+     * @returns {Promise<void>}
+     */
+    const loadUsageData = async () => {
+        try {
+            const data = await Awesome.StorageMgr.get(USER_USAGE_DATA_KEY);
+            if (data) {
+                usageData = JSON.parse(data);
+            }
+        } catch (error) {
+            console.error('加载使用数据失败:', error);
+        }
+    };
+    
+    /**
+     * 保存使用数据到本地存储
+     * @returns {Promise<void>}
+     */
+    const saveUsageData = async () => {
+        try {
+            await Awesome.StorageMgr.set(USER_USAGE_DATA_KEY, JSON.stringify(usageData));
+        } catch (error) {
+            console.error('保存使用数据失败:', error);
+        }
+    };
+    
+    /**
+     * 获取客户端详细信息(仅background可用字段,兼容service worker环境,字段与服务端一致)
+     * @returns {Object}
+     */
+    const getClientInfo = async () => {
+        let tabInfo = {};
+        try {
+            // 获取当前活动tab的页面信息
+            const tabs = await new Promise(resolve => {
+                chrome.tabs.query({active: true, currentWindow: true}, resolve);
+            });
+            if (tabs && tabs.length > 0) {
+                const tab = tabs[0];
+                tabInfo = {
+                    pageUrl: tab.url || '',
+                    pageTitle: tab.title || ''
+                };
+            }
+        } catch (e) {
+            // 忽略tab获取异常
+        }
+        const nav = self.navigator || {};
+        // 只采集服务端需要的字段
+        return {
+            userAgent: nav.userAgent || '',
+            language: nav.language || '',
+            platform: nav.platform || '',
+            extensionVersion: chrome.runtime.getManifest().version,
+            ...tabInfo
+        };
+    };
+
+    /**
+     * 使用自建服务器发送事件数据
+     * @param {string} eventName - 事件名称
+     * @param {Object} params - 事件参数
+     */
+    const sendToServer = async (eventName, params = {}) => {
+        const uid = await getUserId();
+        const clientInfo = await getClientInfo();
+        // 只保留服务端 TrackSchema 需要的字段
+        const payload = {
+            event: eventName,
+            userId: uid,
+            ...clientInfo
+        };
+        // 只允许 TrackSchema 里的字段
+        const allowedFields = [
+            'tool_name', 'extensionVersion', 'browser', 'browserVersion', 'os', 'osVersion',
+            'IP', 'country', 'province', 'city', 'pageUrl', 'pageTitle', 'language', 'platform'
+        ];
+        for (const key of allowedFields) {
+            if (params[key] !== undefined) {
+                payload[key] = params[key];
+            }
+        }
+        try {
+            fetch(SERVER_TRACK_URL, {
+                method: 'POST',
+                body: JSON.stringify(payload),
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                keepalive: true
+            }).catch(e => console.log('自建统计服务器发送失败:', e));
+        } catch (error) {
+            console.log('自建统计发送失败:', error);
+        }
+    };
+    
+    /**
+     * 记录每日活跃用户
+     * @returns {Promise<void>}
+     */
+    const recordDailyActiveUser = async () => {
+        try {
+            // 获取上次活跃日期
+            const lastActiveDate = await Awesome.StorageMgr.get(LAST_ACTIVE_DATE_KEY);
+            
+            // 如果今天还没有记录,则记录今天的活跃
+            if (lastActiveDate !== todayStr) {
+                await Awesome.StorageMgr.set(LAST_ACTIVE_DATE_KEY, todayStr);
+                
+                // 确保该日期的记录存在
+                if (!usageData.dailyUsage[todayStr]) {
+                    usageData.dailyUsage[todayStr] = {
+                        date: todayStr,
+                        tools: {}
+                    };
+                }
+                
+                // 发送每日活跃记录到自建服务器
+                sendToServer('daily_active_user', {
+                    date: todayStr
+                });
+            }
+        } catch (error) {
+            console.error('记录日活跃用户失败:', error);
+        }
+    };
+    
+    /**
+     * 记录插件安装事件
+     */
+    const recordInstallation = async () => {
+        sendToServer('extension_installed');
+    };
+    
+    /**
+     * 记录插件更新事件
+     * @param {string} previousVersion - 更新前的版本
+     */
+    const recordUpdate = async (previousVersion) => {
+        sendToServer('extension_updated', {
+            previous_version: previousVersion
+        });
+    };
+    
+    /**
+     * 记录插件卸载事件
+     */
+    const recordUninstall = async () => {
+        sendToServer('extension_uninstall');
+    };
+    
+    /**
+     * 记录工具使用情况
+     * @param {string} toolName - 工具名称
+     */
+    const recordToolUsage = async (toolName, params = {}) => {
+        // 确保今天的记录存在
+        if (!usageData.dailyUsage[todayStr]) {
+            usageData.dailyUsage[todayStr] = {
+                date: todayStr,
+                tools: {}
+            };
+        }
+        
+        // 增加工具使用计数
+        if (!usageData.tools[toolName]) {
+            usageData.tools[toolName] = 0;
+        }
+        usageData.tools[toolName]++;
+        
+        // 增加今天该工具的使用计数
+        if (!usageData.dailyUsage[todayStr].tools[toolName]) {
+            usageData.dailyUsage[todayStr].tools[toolName] = 0;
+        }
+        usageData.dailyUsage[todayStr].tools[toolName]++;
+        
+        // 保存使用数据
+        await saveUsageData();
+        
+        // 发送工具使用记录到自建服务器
+        sendToServer('tool_used', {
+            tool_name: toolName,
+            date: todayStr,
+            ...params
+        });
+    };
+    
+    /**
+     * 定期发送使用摘要数据
+     */
+    const scheduleSyncStats = () => {
+        // 每周发送一次摘要数据
+        const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
+        setInterval(async () => {
+            // 发送工具使用排名
+            const toolRanking = Object.entries(usageData.tools)
+                .sort((a, b) => b[1] - a[1])
+                .slice(0, 5)
+                .map(([name, count]) => ({name, count}));
+            
+            sendToServer('usage_summary', {
+                top_tools: JSON.stringify(toolRanking)
+            });
+            
+            // 清理过旧的日期数据(保留30天数据)
+            const now = new Date();
+            const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+            const thirtyDaysAgoStr = thirtyDaysAgo.toISOString().split('T')[0];
+            
+            Object.keys(usageData.dailyUsage).forEach(date => {
+                if (date < thirtyDaysAgoStr) {
+                    delete usageData.dailyUsage[date];
+                }
+            });
+            
+            // 保存清理后的数据
+            await saveUsageData();
+        }, ONE_WEEK);
+    };
+    
+    /**
+     * 初始化统计模块
+     */
+    const init = async () => {
+        await getUserId();
+        await loadUsageData();
+        await recordDailyActiveUser();
+        scheduleSyncStats();
+    };
+    
+    /**
+     * 获取最近使用的工具(按最近使用时间倒序,默认最近10个)
+     * @param {number} limit - 返回的最大数量
+     * @returns {Promise<string[]>} 工具名称数组
+     */
+    const getRecentUsedTools = async (limit = 10) => {
+        // 确保数据已加载
+        await loadUsageData();
+        // 收集所有日期,按新到旧排序
+        const dates = Object.keys(usageData.dailyUsage).sort((a, b) => b.localeCompare(a));
+        const toolSet = [];
+        for (const date of dates) {
+            const tools = Object.keys(usageData.dailyUsage[date].tools || {});
+            for (const tool of tools) {
+                if (!toolSet.includes(tool)) {
+                    toolSet.push(tool);
+                    if (toolSet.length >= limit) {
+                        return toolSet;
+                    }
+                }
+            }
+        }
+        return toolSet;
+    };
+    
+    /**
+     * 获取DashBoard统计数据
+     * @returns {Promise<Object>} 统计数据对象
+     */
+    const getDashboardData = async () => {
+        await loadUsageData();
+        // 最近10次使用的工具及时间
+        const recent = [];
+        const recentDetail = [];
+        const dates = Object.keys(usageData.dailyUsage).sort((a, b) => b.localeCompare(a));
+        for (const date of dates) {
+            for (const tool of Object.keys(usageData.dailyUsage[date].tools || {})) {
+                if (!recent.includes(tool)) {
+                    recent.push(tool);
+                    recentDetail.push({ tool, date });
+                    if (recent.length >= 10) break;
+                }
+            }
+            if (recent.length >= 10) break;
+        }
+        // 工具使用总次数排行
+        const mostUsed = Object.entries(usageData.tools)
+            .sort((a, b) => b[1] - a[1])
+            .slice(0, 10)
+            .map(([name, count]) => ({ name, count }));
+        const totalCount = Object.values(usageData.tools).reduce((a, b) => a + b, 0);
+        const activeDays = Object.keys(usageData.dailyUsage).length;
+        const allDates = Object.keys(usageData.dailyUsage).sort();
+        // 最近10天每日使用情况
+        const dailyTrend = allDates.slice(-10).map(date => ({
+            date,
+            count: Object.values(usageData.dailyUsage[date].tools || {}).reduce((a, b) => a + b, 0)
+        }));
+        // 首次和最近活跃日期
+        const firstDate = allDates[0] || '';
+        const lastDate = allDates[allDates.length - 1] || '';
+        // 连续活跃天数
+        let maxStreak = 0, curStreak = 0, prev = '';
+        for (let i = 0; i < allDates.length; i++) {
+            if (i === 0 || (new Date(allDates[i]) - new Date(prev) === 86400000)) {
+                curStreak++;
+            } else {
+                maxStreak = Math.max(maxStreak, curStreak);
+                curStreak = 1;
+            }
+            prev = allDates[i];
+        }
+        maxStreak = Math.max(maxStreak, curStreak);
+        // 本月/本周统计
+        const now = new Date();
+        const thisMonth = now.toISOString().slice(0, 7);
+        const thisWeekMonday = new Date(now.setDate(now.getDate() - now.getDay() + 1)).toISOString().slice(0, 10);
+        let monthCount = 0, weekCount = 0;
+        allDates.forEach(date => {
+            const cnt = Object.values(usageData.dailyUsage[date].tools || {}).reduce((a, b) => a + b, 0);
+            if (date.startsWith(thisMonth)) monthCount += cnt;
+            if (date >= thisWeekMonday) weekCount += cnt;
+        });
+        // 平均每日使用次数
+        const avgPerDay = activeDays ? Math.round(totalCount / activeDays * 10) / 10 : 0;
+        // 最活跃的一天
+        let maxDay = { date: '', count: 0 };
+        allDates.forEach(date => {
+            const cnt = Object.values(usageData.dailyUsage[date].tools || {}).reduce((a, b) => a + b, 0);
+            if (cnt > maxDay.count) maxDay = { date, count: cnt };
+        });
+        // 最近未使用天数
+        let daysSinceLast = 0;
+        if (lastDate) {
+            const diff = Math.floor((new Date() - new Date(lastDate)) / 86400000);
+            daysSinceLast = diff > 0 ? diff : 0;
+        }
+        return {
+            recent,
+            recentDetail,
+            mostUsed,
+            totalCount,
+            activeDays,
+            dailyTrend,
+            firstDate,
+            lastDate,
+            maxStreak,
+            monthCount,
+            weekCount,
+            avgPerDay,
+            maxDay,
+            daysSinceLast,
+            allDates
+        };
+    };
+    
+    return {
+        init,
+        recordInstallation,
+        recordUpdate,
+        recordToolUsage,
+        getRecentUsedTools,
+        getDashboardData,
+        recordUninstall
+    };
+})();
+
+export default Statistics; 

+ 2 - 0
apps/chart-maker/index.html

@@ -27,6 +27,8 @@
                 <span class="fehelper-text">FeHelper</span>
             </a>
             <span class="page-title-suffix">:图表生成器</span>
+            <a href="#" class="x-donate-link"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+            <a class="x-other-tools"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
         </div>
     </div>
 

+ 19 - 1
apps/chart-maker/main.js

@@ -8,10 +8,12 @@ document.addEventListener('DOMContentLoaded', function() {
     const exportPngBtn = document.getElementById('export-png-btn');
     const exportJpgBtn = document.getElementById('export-jpg-btn');
     const copyImgBtn = document.getElementById('copy-img-btn');
-    const chartTypeSelect = document.getElementById('chart-type');
     const manualFormatSelect = document.getElementById('manual-format');
     const fileUploadInput = document.getElementById('file-upload');
     const manualFormatContainer = document.getElementById('manual-format-container');
+    const donateLink = document.querySelector('.x-donate-link');
+    const otherToolsLink = document.querySelector('.x-other-tools');
+
     const manualInputContainers = [simpleDataContainer, seriesDataContainer, csvDataContainer];
     
     // 初始化显示状态
@@ -55,6 +57,22 @@ document.addEventListener('DOMContentLoaded', function() {
         });
     });
 
+    // 监听打赏链接点击事件
+    donateLink.addEventListener('click', function(event) {
+        event.preventDefault();
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'open-donate-modal', 
+            params: { toolName: 'chart-maker' }
+        });
+    });
+
+    // 监听探索更多工具链接点击事件
+    otherToolsLink.addEventListener('click', function(event) {
+        event.preventDefault(); 
+        chrome.runtime.openOptionsPage();
+    });
+
     // 监听手动格式选择变化
     manualFormatSelect.addEventListener('change', function() {
         const format = this.value;

+ 90 - 6
apps/chart-maker/style.css

@@ -14,8 +14,7 @@ body {
     background-image: linear-gradient(to right, rgba(245, 247, 250, 0.8) 1px, transparent 1px),
                       linear-gradient(to bottom, rgba(245, 247, 250, 0.8) 1px, transparent 1px);
     background-size: 20px 20px;
-    padding-top: 15px;
-    max-width: 1200px;
+    
     margin: 0 auto;
     font-size:100%;
 }
@@ -613,6 +612,7 @@ canvas#chart-canvas[data-chart-rendered] {
     display: flex;
     align-items: center;
     margin-bottom: 20px;
+    position: relative;
 }
 
 .page-header .header-link {
@@ -621,7 +621,7 @@ canvas#chart-canvas[data-chart-rendered] {
     text-decoration: none;
     color: #5a5c69;
     font-weight: 500;
-    font-size: 1.2rem;
+    font-size: 14px;
     transition: color 0.2s ease-in-out;
 }
 
@@ -641,6 +641,90 @@ canvas#chart-canvas[data-chart-rendered] {
 
 .page-header .page-title-suffix {
     margin-left: 8px;
-    font-size: 1.2rem;
-    color: #858796;
-} 
+    font-size: 14px;
+    font-weight: 500;
+} 
+/* Style for the "Explore More Tools" button */
+.page-header>a.x-other-tools {
+    margin: 0; /* Reset margin */
+    font-size: 12px; /* Slightly smaller */
+    cursor: pointer;
+    text-decoration: none;
+    -webkit-user-select: none;
+    user-select: none;
+    color: #495057; /* Grey text */
+    background-color: #f8f9fa; /* Light grey background */
+    padding: 6px 12px; /* Adjusted padding */
+    border-radius: 6px; /* Standard rounded corners */
+    border: 1px solid #dee2e6; /* Match border color */
+    transition: all 0.2s ease;
+    display: inline-flex;
+    align-items: center;
+    white-space: nowrap;
+    position: absolute;
+    right: 10px;
+}
+
+.page-header>a.x-other-tools .icon-plus-circle {
+    display: inline-block;
+    width: 14px; /* Slightly smaller icon */
+    height: 14px;
+    background: url(../static/img/plus-circle.svg) no-repeat center center;
+    background-size: contain;
+    margin-right: 4px;
+}
+
+.page-header>a.x-other-tools .tool-market-badge {
+    display: inline-block;
+    background-color: #007bff; /* Match theme blue */
+    color: white;
+    padding: 2px 7px;
+    border-radius: 4px; /* Slightly less round */
+    margin-left: 6px;
+    font-size: 11px; /* Smaller badge text */
+    font-weight: 500;
+}
+
+.page-header>a.x-other-tools:hover {
+    color: #212529;
+    background-color: #e9ecef;
+    border-color: #ced4da;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+    transform: none; /* Remove translateY */
+}
+/* 保持原有的顶部导航样式 */
+.x-donate-link {
+    font-size: 14px;
+    font-weight: 500;
+    color: #2563eb;
+    cursor: pointer;
+    text-decoration: none;
+    border: none;
+    margin-left: 70px;
+    white-space: nowrap;
+    margin-right: auto;
+    padding: 5px 16px;
+    border-radius: 20px;
+    background-color: #eff6ff;
+    transition: all 0.2s ease;
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    box-shadow: 0 1px 2px rgba(37, 99, 235, 0.1);
+}
+
+.x-donate-link:hover {
+    background-color: #dbeafe;
+    color: #1d4ed8;
+    text-decoration: none;
+    box-shadow: 0 2px 4px rgba(37, 99, 235, 0.15);
+    transform: translateY(-1px);
+}
+
+.x-donate-link {
+    margin: 0 10px;
+    font-size: 12px;
+    font-weight: normal;
+    position: absolute;
+    right: 220px;
+}

+ 35 - 11
apps/code-beautify/content-script.js

@@ -6,13 +6,35 @@ window.codebeautifyContentScript = (() => {
         if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
             url = chrome.runtime.getURL('code-beautify/' + filename);
         }
-        fetch(url).then(resp => resp.text()).then(jsText => {
-            if(window.evalCore && window.evalCore.getEvalInstance){
-                return window.evalCore.getEvalInstance(window)(jsText);
+        // 使用chrome.runtime.sendMessage向background请求加载脚本
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'load-local-script',
+            script: url
+        }, (scriptContent) => {
+            if (!scriptContent) {
+                console.error('Failed to load script:', filename);
+                return;
+            }
+            
+            // 如果有evalCore则使用它
+            if (window.evalCore && window.evalCore.getEvalInstance) {
+                try {
+                    window.evalCore.getEvalInstance(window)(scriptContent);
+                } catch(e) {
+                    console.error('Failed to evaluate script using evalCore:', e);
+                }
+            } else {
+                // 创建一个函数来执行脚本
+                try {
+                    // 使用Function构造函数创建一个函数,并在当前窗口上下文中执行
+                    // 这比动态创建script元素更安全,因为它不涉及DOM操作
+                    const executeScript = new Function(scriptContent);
+                    executeScript.call(window);
+                } catch(e) {
+                    console.error('Failed to execute script:', filename, e);
+                }
             }
-            let el = document.createElement('script');
-            el.textContent = jsText;
-            document.head.appendChild(el);
         });
     };
 
@@ -191,11 +213,13 @@ window.codebeautifyContentScript = (() => {
             fileType = undefined;
         }
 
-        chrome.runtime.sendMessage({
-            type: 'fh-dynamic-any-thing',
-            thing: 'code-beautify',
-            params: { fileType, tabId: window.__FH_TAB_ID__ || null }
-        });
+        if (['javascript', 'css'].includes(fileType)) {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'code-beautify',
+                params: { fileType, tabId: window.__FH_TAB_ID__ || null }
+            });
+        }
     };
 
 })();

+ 37 - 0
apps/en-decode/index.css

@@ -271,4 +271,41 @@ td.td-label {
     background-color: #e6edff;
     box-shadow: 0 2px 5px rgba(0,0,0,0.15);
     transform: translateY(-1px);
+}
+
+/* 保持原有的顶部导航样式 */
+.x-donate-link {
+    font-size: 14px;
+    font-weight: 500;
+    color: #2563eb;
+    cursor: pointer;
+    text-decoration: none;
+    border: none;
+    white-space: nowrap;
+    margin-right: auto;
+    border-radius: 20px;
+    background-color: #eff6ff;
+    transition: all 0.2s ease;
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    box-shadow: 0 1px 2px rgba(37, 99, 235, 0.1);
+    position: absolute;
+    right: 230px;
+    top: 8px;
+    padding: 6px 12px;
+}
+
+.x-donate-link:hover {
+    background-color: #dbeafe;
+    color: #1d4ed8;
+    text-decoration: none;
+    box-shadow: 0 2px 4px rgba(37, 99, 235, 0.15);
+    transform: translateY(-1px);
+}
+
+.x-donate-link {
+    margin: 0 10px;
+    font-size: 12px;
+    font-weight: normal;
 }

+ 1 - 1
apps/en-decode/index.html

@@ -16,7 +16,7 @@
                 <h3 class="panel-title">
                     <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
                         <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:信息编解码工具
-
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
                     <a class="x-other-tools" @click="openOptionsPage()"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                 </h3>
             </div>

+ 9 - 0
apps/en-decode/index.js

@@ -123,6 +123,15 @@ new Vue({
 
         openOptionsPage: function() {
             chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event ){
+            event.preventDefault();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'en-decode' }
+            });
         }
     }
 });

+ 9 - 0
apps/json-format/content-script.css

@@ -591,6 +591,15 @@ html.fh-jf svg:not(:root) {
 .fe-feedback>a.x-other-tools-us {
     display: none;
 }
+.x-toolbar>.x-donate-link {
+    margin-left: 20px;
+}
+.x-toolbar>.x-donate-link-us {
+    display: none;
+}
+.x-toolbar>.x-donate-link a:hover {
+    color: #f00;
+}
 
 .fe-feedback>a.x-other-tools .icon-plus-circle {
     display: inline-block;

+ 62 - 6
apps/json-format/content-script.js

@@ -14,13 +14,36 @@ window.JsonAutoFormat = (() => {
         if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
             url = chrome.runtime.getURL('json-format/' + filename);
         }
-        fetch(url).then(resp => resp.text()).then(jsText => {
-            if(window.evalCore && window.evalCore.getEvalInstance){
-                return window.evalCore.getEvalInstance(window)(jsText);
+        
+        // 使用chrome.runtime.sendMessage向background请求加载脚本
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'load-local-script',
+            script: url
+        }, (scriptContent) => {
+            if (!scriptContent) {
+                console.error('Failed to load script:', filename);
+                return;
+            }
+            
+            // 如果有evalCore则使用它
+            if (window.evalCore && window.evalCore.getEvalInstance) {
+                try {
+                    window.evalCore.getEvalInstance(window)(scriptContent);
+                } catch(e) {
+                    console.error('Failed to evaluate script using evalCore:', e);
+                }
+            } else {
+                // 创建一个函数来执行脚本
+                try {
+                    // 使用Function构造函数创建一个函数,并在当前窗口上下文中执行
+                    // 这比动态创建script元素更安全,因为它不涉及DOM操作
+                    const executeScript = new Function(scriptContent);
+                    executeScript.call(window);
+                } catch(e) {
+                    console.error('Failed to execute script:', filename, e);
+                }
             }
-            let el = document.createElement('script');
-            el.textContent = jsText;
-            document.head.appendChild(el);
         });
     };
 
@@ -118,6 +141,7 @@ window.JsonAutoFormat = (() => {
             '    </span>' +
             '    <span class="x-fix-encoding"><span class="x-split">|</span><button class="xjf-btn" id="jsonGetCorrectCnt">乱码修正</button></span>' +
             '    <span id="optionBar"></span>' +
+            '    <span class="x-donate-link' + (isInUSA() ? ' x-donate-link-us' : '') + '"><a href="#" id="donateLink"><i class="nav-icon">❤</i>打赏鼓励</a></span>' +
             '    <span class="fe-feedback">' +
             '       <span class="x-settings"><svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 14 16" width="14">' +
             '           <path fill-rule="evenodd" d="M14 8.77v-1.6l-1.94-.64-.45-1.09.88-1.84-1.13-1.13-1.81.91-1.09-.45-.69-1.92h-1.6l-.63 1.94-1.11.45-1.84-.88-1.13 1.13.91 1.81-.45 1.09L0 7.23v1.59l1.94.64.45 1.09-.88 1.84 1.13 1.13 1.81-.91 1.09.45.69 1.92h1.59l.63-1.94 1.11-.45 1.84.88 1.13-1.13-.92-1.81.47-1.09L14 8.75v.02zM7 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path>' +
@@ -339,6 +363,15 @@ window.JsonAutoFormat = (() => {
 
         $('.fe-feedback .x-settings').click(e => _createSettingPanel());
         $('#jsonGetCorrectCnt').click(e => _getCorrectContent());
+
+        $('.x-toolbar .x-donate-link').on('click', function (e) {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'json-format' }
+            });
+        });
+        
     };
 
     let _didFormat = function () {
@@ -404,6 +437,17 @@ window.JsonAutoFormat = (() => {
                 $('#jfCallbackName_end').html(')');
             }
         }
+        
+        // 埋点:自动触发json-format-auto
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'statistics-tool-usage',
+            params: {
+                tool_name: 'json-format',
+                url: location.href
+            }
+        });
+        
     };
 
     let _getCorrectContent = function () {
@@ -666,6 +710,18 @@ window.JsonAutoFormat = (() => {
         }
     };
 
+    // 页面加载后自动采集
+    try {
+        if (window.chrome && chrome.runtime && chrome.runtime.sendMessage && window.Awesome && window.Awesome.collectAndSendClientInfo) {
+            window.Awesome.collectAndSendClientInfo();
+        } else {
+            // fallback: 动态加载Awesome模块
+            import(chrome.runtime.getURL('background/awesome.js')).then(module => {
+                module.default.collectAndSendClientInfo();
+            }).catch(() => {});
+        }
+    } catch(e) {}
+
     return {
         format: () => _getAllOptions(options => {
             if(options.JSON_PAGE_FORMAT) {

+ 3 - 2
apps/json-format/index.css

@@ -33,15 +33,16 @@ html,body {
     opacity: 0.3;
     margin: 5px 0 0 -8px;
 }
-.x-xdemo,a.x-xdemo {
+.x-xdemo,a.x-xdemo,.x-donate-link>a {
     margin-left: 30px;
     font-size: 12px;
     color: blue;
     cursor: pointer;
     text-decoration: underline;
 }
-.x-xdemo:hover {
+.x-xdemo:hover, .x-donate-link>a:hover {
     text-decoration: underline;
+    color: #f00;
 }
 
 #errorTips {

+ 2 - 2
apps/json-format/index.html

@@ -20,8 +20,8 @@
                         <span id="layoutBar">
                             <button id="btnLeftRight" ref="btnLeftRight" class="selected" @click="changeLayout('left-right')">左右布局</button><button id="btnUpDown" ref="btnUpDown" @click="changeLayout('up-down')">上下布局</button>
                         </span>
-
-                        <a class="x-other-tools" @click="openOptionsPage()"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                        <span class="x-donate-link" v-if="!isInUSAFlag" @click="openDonateModal()"><a href="#" id="donateLink"><i class="nav-icon">❤</i>打赏鼓励</a></span>
+                        <a class="x-other-tools" v-if="!isInUSAFlag" @click="openOptionsPage()"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                     </h3>
                 </div>
             </div>

+ 25 - 1
apps/json-format/index.js

@@ -23,7 +23,8 @@ new Vue({
         jsonLintSwitch: true,
         autoDecode: false,
         fireChange: true,
-        overrideJson: false
+        overrideJson: false,
+        isInUSAFlag: false
     },
     mounted: function () {
         // 自动开关灯控制
@@ -34,6 +35,8 @@ new Vue({
         this.autoDecode = localStorage.getItem(AUTO_DECODE);
         this.autoDecode = this.autoDecode === 'true';
 
+        this.isInUSAFlag = this.isInUSA();
+
         this.jsonLintSwitch = (localStorage.getItem(JSON_LINT) !== 'false');
         this.overrideJson = (localStorage.getItem(EDIT_ON_CLICK) === 'true');
         this.changeLayout(localStorage.getItem(LOCAL_KEY_OF_LAYOUT));
@@ -77,6 +80,19 @@ new Vue({
         }
     },
     methods: {
+        isInUSA: function () {
+            // 通过时区判断是否在美国
+            const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+            const isUSTimeZone = /^America\/(New_York|Chicago|Denver|Los_Angeles|Anchorage|Honolulu)/.test(timeZone);
+
+            // 通过语言判断
+            const language = navigator.language || navigator.userLanguage;
+            const isUSLanguage = language.toLowerCase().indexOf('en-us') > -1;
+
+            // 如果时区和语言都符合美国特征,则认为在美国
+            return (isUSTimeZone && isUSLanguage);
+        },
+
         format: function () {
             this.errorMsg = '';
             this.placeHolder = this.defaultResultTpl;
@@ -286,6 +302,14 @@ new Vue({
             chrome.runtime.openOptionsPage();
         },
 
+        openDonateModal: function(){
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'json-format' }
+            });
+        },
+
         setDemo: function () {
             let demo = '{"BigIntSupported":995815895020119788889,"date":"20180322","message":"Success !","status":200,"city":"北京","count":632,"data":{"shidu":"34%","pm25":73,"pm10":91,"quality":"良","wendu":"5","ganmao":"极少数敏感人群应减少户外活动","yesterday":{"date":"21日星期三","sunrise":"06:19","high":"高温 11.0℃","low":"低温 1.0℃","sunset":"18:26","aqi":85,"fx":"南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},"forecast":[{"date":"22日星期四","sunrise":"06:17","high":"高温 17.0℃","low":"低温 1.0℃","sunset":"18:27","aqi":98,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"23日星期五","sunrise":"06:16","high":"高温 18.0℃","low":"低温 5.0℃","sunset":"18:28","aqi":118,"fx":"无持续风向","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},{"date":"24日星期六","sunrise":"06:14","high":"高温 21.0℃","low":"低温 7.0℃","sunset":"18:29","aqi":52,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"25日星期日","sunrise":"06:13","high":"高温 22.0℃","low":"低温 7.0℃","sunset":"18:30","aqi":71,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"26日星期一","sunrise":"06:11","high":"高温 21.0℃","low":"低温 8.0℃","sunset":"18:31","aqi":97,"fx":"西南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"}]}}';
             editor.setValue(demo);

+ 3 - 0
apps/manifest.json

@@ -61,6 +61,9 @@
             "static/vendor/evalCore.min.js",
             "screenshot/content-script.js",
 
+            "background/awesome.js",
+            "background/tools.js",
+
             "code-beautify/beautify.js",
             "code-beautify/beautify-css.js",
 

+ 3 - 2
apps/options/index.html

@@ -273,13 +273,14 @@
                         </li>
                         <li @click="showRecentUsed">
                             最近使用
-                            <span class="count">({{getRecentCount()}})</span>
+                            <span class="count">({{recentCount}})</span>
                         </li>
                     </ul>
                 </div>
             </div>
 
             <!-- 工具展示区 -->
+            <div id="fh-dashboard-panel" style="display:none;"></div>
             <div class="tools-grid">
                 <div :class="['tools-container', viewMode]">
                     <div v-for="tool in filteredTools" 
@@ -320,7 +321,7 @@
 </div>
 
 <script src="../static/js/dark-mode.js"></script>
-<script type="module" src="market.js"></script>
+<script type="module" src="index.js"></script>
 
 </body>
 </html> 

+ 1391 - 0
apps/options/index.js

@@ -0,0 +1,1391 @@
+import Awesome from '../background/awesome.js'
+import MSG_TYPE from '../static/js/common.js';
+import Settings from './settings.js';
+import Statistics from '../background/statistics.js';
+import toolMap from '../background/tools.js';
+
+// 工具分类定义
+const TOOL_CATEGORIES = [
+    { key: 'dev', name: '开发工具类', tools: ['json-format', 'json-diff', 'code-beautify', 'code-compress', 'postman', 'websocket', 'regexp','page-timing'] },
+    { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'timestamp', 'trans-color'] },
+    { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'svg-converter', 'chart-maker', 'poster-maker' ,'screenshot', 'color-picker'] },
+    { key: 'productivity', name: '效率工具类', tools: ['aiagent', 'sticky-notes', 'html2markdown', 'page-monkey'] },
+    { key: 'calculator', name: '计算工具类', tools: ['crontab', 'loan-rate', 'password'] },
+    { key: 'other', name: '其他工具', tools: [] }
+];
+
+// Vue实例
+new Vue({
+    el: '#marketContainer',
+    data: {
+        manifest: { version: '0.0.0' },
+        searchKey: '',
+        currentCategory: '',
+        sortType: 'default',
+        viewMode: 'list', // 默认网格视图
+        categories: TOOL_CATEGORIES,
+        favorites: new Set(),
+        recentUsed: [],
+        loading: true,
+        originalTools: {}, // 保存原始工具数据
+        currentView: 'all', // 当前视图类型(all/installed/favorites/recent)
+        activeTools: {}, // 当前显示的工具列表
+        installedCount: 0, // 已安装工具数量
+        
+        // 版本相关
+        latestVersion: '', // 最新版本号
+        needUpdate: false, // 是否需要更新
+        
+        // 设置相关
+        showSettingsModal: false,
+        defaultKey: 'Alt+Shift+J', // 默认快捷键
+        countDown: 0, // 夜间模式倒计时
+        selectedOpts: [], // 选中的选项
+        menuDownloadCrx: false, // 菜单-插件下载
+        menuFeHelperSeting: false, // 菜单-FeHelper设置
+        isFirefox: false, // 是否Firefox浏览器
+
+        // 打赏相关
+        showDonateModal: false,
+        donate: {
+            text: '感谢你对FeHelper的认可和支持!',
+            image: './donate.jpeg'
+        },
+
+        // 确认对话框
+        confirmDialog: {
+            show: false,
+            title: '操作确认',
+            message: '',
+            callback: null,
+            data: null
+        },
+
+        recentCount: 0,
+        showDashboard: false, // 是否显示DashBoard
+        dashboardData: null, // DashBoard数据
+    },
+
+    async created() {
+        await this.initData();
+        this.recentCount = (await Statistics.getRecentUsedTools(10)).length;
+        // 初始化后更新已安装工具数量
+        this.updateInstalledCount();
+        // 恢复用户的视图模式设置
+        this.loadViewMode();
+        // 加载设置项
+        this.loadSettings();
+        // 检查浏览器类型
+        this.checkBrowserType();
+        // 检查版本更新
+        this.checkVersionUpdate();
+        
+        // 检查URL中是否有donate_from参数
+        this.checkDonateParam();
+
+        // 埋点:自动触发options
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'statistics-tool-usage',
+            params: {
+                tool_name: 'options'
+            }
+        });
+    },
+
+    computed: {
+        filteredTools() {
+            if (this.loading) {
+                return [];
+            }
+
+            // 获取当前工具列表
+            let result = Object.values(this.activeTools).map(tool => ({
+                ...tool,
+                favorite: this.favorites.has(tool.key)
+            }));
+
+            // 搜索过滤
+            if (this.searchKey) {
+                const key = this.searchKey.toLowerCase();
+                result = result.filter(tool => 
+                    tool.name.toLowerCase().includes(key) || 
+                    tool.tips.toLowerCase().includes(key)
+                );
+            }
+
+            // 分类过滤,在所有视图下生效
+            if (this.currentCategory) {
+                const category = TOOL_CATEGORIES.find(c => c.key === this.currentCategory);
+                const categoryTools = category ? category.tools : [];
+                result = result.filter(tool => categoryTools.includes(tool.key));
+            }
+
+            // 排序
+            switch (this.sortType) {
+                case 'newest':
+                    result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
+                    break;
+                case 'hot':
+                    result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
+                    break;
+                default:
+                    const allTools = TOOL_CATEGORIES.reduce((acc, category) => {
+                        acc.push(...category.tools);
+                        return acc;
+                    }, []);
+                    
+                    result.sort((a, b) => {
+                        const indexA = allTools.indexOf(a.key);
+                        const indexB = allTools.indexOf(b.key);
+                        
+                        // 如果工具不在任何类别中,放到最后
+                        if (indexA === -1 && indexB === -1) {
+                            return a.key.localeCompare(b.key); // 字母顺序排序
+                        }
+                        if (indexA === -1) return 1;
+                        if (indexB === -1) return -1;
+                        
+                        return indexA - indexB;
+                    });
+            }
+
+            return result;
+        }
+    },
+
+    methods: {
+        async initData() {
+            try {
+                this.loading = true;
+
+                // 获取manifest信息
+                const manifest = await chrome.runtime.getManifest();
+                this.manifest = manifest;
+
+                // 从 Awesome.getAllTools 获取工具列表
+                const tools = await Awesome.getAllTools();
+                
+                // 获取收藏数据
+                const favorites = await this.getFavoritesData();
+                this.favorites = new Set(favorites);
+
+                // 获取最近使用数据
+                const recentUsed = await this.getRecentUsedData();
+                this.recentUsed = recentUsed;
+                this.recentCount = recentUsed.length;
+
+                // 获取已安装工具列表
+                const installedTools = await Awesome.getInstalledTools();
+
+                // 处理工具数据
+                const processedTools = {};
+                Object.entries(tools).forEach(([key, tool]) => {
+                    // 检查工具是否已安装
+                    const isInstalled = installedTools.hasOwnProperty(key);
+                    // 检查是否有右键菜单
+                    const hasMenu = tool.menu || false;
+                    
+                    processedTools[key] = {
+                        ...tool,
+                        key, // 添加key到工具对象中
+                        updateTime: Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000,
+                        installed: isInstalled, // 使用实时安装状态
+                        inContextMenu: hasMenu, // 使用实时菜单状态
+                        systemInstalled: tool.systemInstalled || false, // 是否系统预装
+                        favorite: this.favorites.has(key)
+                    };
+                });
+
+                this.originalTools = processedTools;
+                
+                // 初始化activeTools为所有工具
+                this.activeTools = { ...processedTools };
+                
+                // 更新"其他工具"类别
+                this.updateOtherCategory(Object.keys(processedTools));
+
+                // 默认选中"全部分类"
+                this.currentCategory = '';
+            } catch (error) {
+                console.error('初始化数据失败:', error);
+            } finally {
+                this.loading = false;
+            }
+        },
+        
+        // 更新"其他工具"类别,将未分类的工具添加到此类别
+        updateOtherCategory(allToolKeys) {
+            // 获取所有已分类的工具
+            const categorizedTools = new Set();
+            TOOL_CATEGORIES.forEach(category => {
+                if (category.key !== 'other') {
+                    category.tools.forEach(tool => categorizedTools.add(tool));
+                }
+            });
+            
+            // 找出未分类的工具
+            const uncategorizedTools = allToolKeys.filter(key => !categorizedTools.has(key));
+            
+            // 更新"其他工具"类别
+            const otherCategory = TOOL_CATEGORIES.find(category => category.key === 'other');
+            if (otherCategory) {
+                otherCategory.tools = uncategorizedTools;
+            }
+        },
+
+        // 检查版本更新
+        async checkVersionUpdate() {
+            try {
+                // 获取已安装的版本号
+                const currentVersion = this.manifest.version;
+                
+                // 尝试从本地存储获取最新版本信息,避免频繁请求
+                const cachedData = await new Promise(resolve => {
+                    chrome.storage.local.get('fehelper_latest_version_data', data => {
+                        resolve(data.fehelper_latest_version_data || null);
+                    });
+                });
+        
+                // 检查是否需要重新获取版本信息:
+                // 1. 缓存不存在
+                // 2. 缓存已过期(超过24小时)
+                // 3. 缓存的当前版本与实际版本不同(说明插件已更新)
+                const now = Date.now();
+                const cacheExpired = !cachedData || !cachedData.timestamp || (now - cachedData.timestamp > 24 * 60 * 60 * 1000);
+                const versionChanged = cachedData && cachedData.currentVersion !== currentVersion;
+                
+                if (cacheExpired || versionChanged) {
+                    try {
+                        console.log('开始获取最新版本信息...');
+                        // 使用shields.io的JSON API获取最新版本号
+                        const response = await fetch('https://img.shields.io/chrome-web-store/v/pkgccpejnmalmdinmhkkfafefagiiiad.json');
+                        if (!response.ok) {
+                            throw new Error(`HTTP错误:${response.status}`);
+                        }
+                        
+                        const data = await response.json();
+                        // 提取版本号 - shields.io返回的数据中包含版本信息
+                        let latestVersion = '';
+                        if (data && data.value) {
+                            // 去掉版本号前的'v'字符(如果有)
+                            latestVersion = data.value.replace(/^v/, '');
+                            console.log('获取到最新版本号:', latestVersion);
+                        }
+                        
+                        // 比较版本号
+                        const needUpdate = this.compareVersions(currentVersion, latestVersion) < 0;
+                        console.log('当前版本:', currentVersion, '最新版本:', latestVersion, '需要更新:', needUpdate);
+                        
+                        // 保存到本地存储中
+                        await chrome.storage.local.set({
+                            'fehelper_latest_version_data': {
+                                timestamp: now,
+                                currentVersion, // 保存当前检查时的版本号
+                                latestVersion,
+                                needUpdate
+                            }
+                        });
+                        
+                        this.latestVersion = latestVersion;
+                        this.needUpdate = needUpdate;
+                    } catch (fetchError) {
+                        console.error('获取最新版本信息失败:', fetchError);
+                        // 获取失败时不显示更新按钮
+                        this.needUpdate = false;
+                        
+                        // 如果是版本变更导致的重新检查,但获取失败,则使用缓存数据
+                        if (versionChanged && cachedData) {
+                            this.latestVersion = cachedData.latestVersion || '';
+                            // 比较新的currentVersion和缓存的latestVersion
+                            this.needUpdate = this.compareVersions(currentVersion, cachedData.latestVersion) < 0;
+                        }
+                    }
+                } else {
+                    // 使用缓存数据
+                    console.log('使用缓存的版本信息');
+                    this.latestVersion = cachedData.latestVersion || '';
+                    this.needUpdate = cachedData.needUpdate || false;
+                }
+            } catch (error) {
+                console.error('检查版本更新失败:', error);
+                this.needUpdate = false; // 出错时不显示更新提示
+            }
+        },
+        
+        // 比较版本号:如果v1 < v2返回-1,v1 = v2返回0,v1 > v2返回1
+        compareVersions(v1, v2) {
+            // 将版本号拆分为数字数组
+            const v1Parts = v1.split('.').map(Number);
+            const v2Parts = v2.split('.').map(Number);
+            
+            // 计算两个版本号中较长的长度
+            const maxLength = Math.max(v1Parts.length, v2Parts.length);
+            
+            // 比较每一部分
+            for (let i = 0; i < maxLength; i++) {
+                // 获取当前部分,如果不存在则视为0
+                const part1 = v1Parts[i] || 0;
+                const part2 = v2Parts[i] || 0;
+                
+                // 比较当前部分
+                if (part1 < part2) return -1;
+                if (part1 > part2) return 1;
+            }
+            
+            // 所有部分都相等
+            return 0;
+        },
+        
+        // 打开Chrome商店页面
+        openStorePage() {
+            try {
+                console.log('开始请求检查更新...');
+                
+                // 使用Chrome Extension API请求检查更新
+                // Manifest V3中requestUpdateCheck返回Promise,结果是一个对象而不是数组
+                chrome.runtime.requestUpdateCheck().then(result => {
+                    // 正确获取status和details,它们是result对象的属性
+                    console.log('更新检查结果:', result);
+                    const status = result.status;
+                    const details = result.details;
+                    
+                    console.log('更新检查状态:', status, '详情:', details);
+                    this.handleUpdateStatus(status, details);
+                }).catch(error => {
+                    console.error('更新检查失败:', error);
+                    this.handleUpdateError(error);
+                });
+            } catch (error) {
+                console.error('请求更新出错:', error);
+                this.handleUpdateError(error);
+            }
+        },
+
+        // 处理更新状态
+        handleUpdateStatus(status, details) {
+            console.log(`处理更新状态: ${status}`, details);
+            
+            if (status === 'update_available') {
+                console.log('发现更新:', details);
+                
+                // 显示更新通知
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '已发现新版本,正在更新...'
+                });
+                
+                // 重新加载扩展以应用更新
+                setTimeout(() => {
+                    console.log('重新加载扩展...');
+                    chrome.runtime.reload();
+                }, 1000);
+            } else if (status === 'no_update') {
+                // 如果没有可用更新,但用户点击了更新按钮
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '您的FeHelper已经是最新版本。'
+                });
+            } else {
+                // 其他情况,如更新检查失败等
+                console.log('其他更新状态:', status);
+                
+                // 备选方案:跳转到官方网站
+                chrome.tabs.create({ 
+                    url: 'https://baidufe.com/fehelper'
+                });
+                
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '自动更新失败,请访问FeHelper官网手动获取最新版本。'
+                });
+            }
+        },
+
+        // 处理更新错误
+        handleUpdateError(error) {
+            console.error('更新过程中出错:', error);
+            
+            // 出错时跳转到官方网站
+            chrome.tabs.create({ 
+                url: 'https://baidufe.com/fehelper'
+            });
+            
+            this.showNotification({
+                title: 'FeHelper 更新错误',
+                message: '更新过程中出现错误,请手动检查更新。'
+            });
+        },
+
+        // 显示通知的统一方法
+        showNotification(options) {
+            try {
+                console.log('准备显示通知:', options);
+                
+                // 定义通知ID,方便后续关闭
+                const notificationId = 'fehelper-update-notification';
+                const simpleNotificationId = 'fehelper-simple-notification';
+
+                // 直接尝试创建通知,不检查权限
+                // Chrome扩展在manifest中已声明notifications权限,应该可以直接使用
+                const notificationOptions = {
+                    type: 'basic',
+                    iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
+                    title: options.title || 'FeHelper',
+                    message: options.message || '',
+                    priority: 2,
+                    requireInteraction: false, // 改为false,因为我们会手动关闭
+                    silent: false // 播放音效
+                };
+                
+                console.log('通知选项:', notificationOptions);
+                
+                // 首先尝试直接创建通知
+                chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
+                    const error = chrome.runtime.lastError;
+                    if (error) {
+                        console.error('创建通知出错:', error);
+                        
+                        // 通知创建失败,尝试使用alert作为备选方案
+                        alert(`${options.title}: ${options.message}`);
+                        
+                        // 再尝试使用不同的选项创建通知
+                        const simpleOptions = {
+                            type: 'basic',
+                            iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
+                            title: options.title || 'FeHelper',
+                            message: options.message || ''
+                        };
+                        
+                        // 使用简化选项再次尝试
+                        chrome.notifications.create(simpleNotificationId, simpleOptions, (simpleId) => {
+                            if (chrome.runtime.lastError) {
+                                console.error('简化通知创建也失败:', chrome.runtime.lastError);
+                            } else {
+                                console.log('简化通知已创建,ID:', simpleId);
+                                
+                                // 3秒后自动关闭简化通知
+                                setTimeout(() => {
+                                    chrome.notifications.clear(simpleId, (wasCleared) => {
+                                        console.log('简化通知已关闭:', wasCleared);
+                                    });
+                                }, 3000);
+                            }
+                        });
+                    } else {
+                        console.log('通知已成功创建,ID:', createdId);
+                        
+                        // 3秒后自动关闭通知
+                        setTimeout(() => {
+                            chrome.notifications.clear(createdId, (wasCleared) => {
+                                console.log('通知已关闭:', wasCleared);
+                            });
+                        }, 3000);
+                    }
+                });
+                
+                // 同时使用内置UI显示消息
+                this.showInPageNotification(options);
+            } catch (error) {
+                console.error('显示通知时出错:', error);
+                // 降级为alert
+                alert(`${options.title}: ${options.message}`);
+            }
+        },
+
+        // 在页面内显示通知消息
+        showInPageNotification(options) {
+            try {
+                // 创建一个通知元素
+                const notificationEl = document.createElement('div');
+                notificationEl.className = 'in-page-notification';
+                notificationEl.innerHTML = `
+                    <div class="notification-content">
+                        <div class="notification-title">${options.title || 'FeHelper'}</div>
+                        <div class="notification-message">${options.message || ''}</div>
+                    </div>
+                    <button class="notification-close">×</button>
+                `;
+                
+                // 添加样式
+                const style = document.createElement('style');
+                style.textContent = `
+                    .in-page-notification {
+                        position: fixed;
+                        bottom: 20px;
+                        right: 20px;
+                        background-color: #4285f4;
+                        color: white;
+                        padding: 15px;
+                        border-radius: 8px;
+                        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+                        z-index: 9999;
+                        display: flex;
+                        align-items: center;
+                        justify-content: space-between;
+                        min-width: 300px;
+                        animation: slideIn 0.3s ease-out;
+                    }
+                    .notification-content {
+                        flex: 1;
+                    }
+                    .notification-title {
+                        font-weight: bold;
+                        margin-bottom: 5px;
+                    }
+                    .notification-message {
+                        font-size: 14px;
+                    }
+                    .notification-close {
+                        background: none;
+                        border: none;
+                        color: white;
+                        font-size: 20px;
+                        cursor: pointer;
+                        margin-left: 10px;
+                        padding: 0 5px;
+                    }
+                    @keyframes slideIn {
+                        from { transform: translateX(100%); opacity: 0; }
+                        to { transform: translateX(0); opacity: 1; }
+                    }
+                    @keyframes slideOut {
+                        from { transform: translateX(0); opacity: 1; }
+                        to { transform: translateX(100%); opacity: 0; }
+                    }
+                `;
+                
+                // 添加到页面
+                document.head.appendChild(style);
+                document.body.appendChild(notificationEl);
+                
+                // 点击关闭按钮移除通知
+                const closeBtn = notificationEl.querySelector('.notification-close');
+                if (closeBtn) {
+                    closeBtn.addEventListener('click', () => {
+                        notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
+                        notificationEl.addEventListener('animationend', () => {
+                            notificationEl.remove();
+                        });
+                    });
+                }
+                
+                // 3秒后自动移除(从5秒改为3秒)
+                setTimeout(() => {
+                    notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
+                    notificationEl.addEventListener('animationend', () => {
+                        notificationEl.remove();
+                    });
+                }, 3000);
+                
+                console.log('页内通知已显示,将在3秒后自动关闭');
+            } catch (error) {
+                console.error('创建页内通知出错:', error);
+            }
+        },
+
+        async getFavoritesData() {
+            return new Promise((resolve) => {
+                chrome.storage.local.get('favorites', (result) => {
+                    resolve(result.favorites || []);
+                });
+            });
+        },
+
+        async getRecentUsedData() {
+            // 直接从Statistics模块获取最近使用的工具
+            return await Statistics.getRecentUsedTools(10);
+        },
+
+        async saveFavorites() {
+            try {
+                await chrome.storage.local.set({
+                    favorites: Array.from(this.favorites)
+                });
+                // 更新工具的收藏状态
+                Object.keys(this.originalTools).forEach(key => {
+                    this.originalTools[key].favorite = this.favorites.has(key);
+                });
+            } catch (error) {
+                console.error('保存收藏失败:', error);
+            }
+        },
+
+        handleSearch() {
+            // 搜索时不重置视图类型,允许在已过滤的结果中搜索
+        },
+
+        handleCategoryChange(category) {
+            // 切换到全部工具视图
+            if (this.currentView !== 'all') {
+                this.currentView = 'all';
+                this.updateActiveTools('all');
+            }
+            this.currentCategory = category;
+            this.searchKey = '';
+            // 确保工具显示正确
+            this.activeTools = { ...this.originalTools };
+            this.showDashboard = false;
+        },
+
+        handleSort() {
+            // 排序逻辑已在computed中实现
+        },
+
+        getCategoryCount(categoryKey) {
+            const category = TOOL_CATEGORIES.find(c => c.key === categoryKey);
+            const categoryTools = category ? category.tools : [];
+            return categoryTools.length;
+        },
+
+        async getInstalledCount() {
+            try {
+                // 使用Awesome.getInstalledTools实时获取已安装工具数量
+                const installedTools = await Awesome.getInstalledTools();
+                return Object.keys(installedTools).length;
+            } catch (error) {
+                console.error('获取已安装工具数量失败:', error);
+                // 回退到本地数据
+                return Object.values(this.originalTools).filter(tool => 
+                    tool.installed || tool.systemInstalled || false
+                ).length;
+            }
+        },
+
+        getFavoritesCount() {
+            return this.favorites.size;
+        },
+
+        getToolCategory(toolKey) {
+            for (const category of TOOL_CATEGORIES) {
+                if (category.tools.includes(toolKey)) {
+                    return category.key;
+                }
+            }
+            return 'other';
+        },
+
+        async showMyInstalled() {
+            this.currentView = 'installed';
+            this.currentCategory = '';
+            this.searchKey = '';
+            await this.updateActiveTools('installed');
+            // 更新已安装工具数量
+            await this.updateInstalledCount();
+            this.showDashboard = false;
+        },
+
+        showMyFavorites() {
+            this.currentView = 'favorites';
+            this.currentCategory = '';
+            this.searchKey = '';
+            this.updateActiveTools('favorites');
+            this.showDashboard = false;
+        },
+
+        async showRecentUsed() {
+            this.currentView = 'recent';
+            this.currentCategory = '';
+            this.searchKey = '';
+            // 拉取DashBoard数据并显示
+            this.dashboardData = await Statistics.getDashboardData();
+            this.showDashboard = true;
+            // 不再更新工具列表
+        },
+
+        // 关闭DashBoard,恢复工具列表
+        closeDashboard() {
+            this.showDashboard = false;
+            this.currentView = 'all';
+            this.updateActiveTools('all');
+        },
+
+        // 重置工具列表到原始状态
+        resetTools() {
+            this.currentView = 'all';
+        },
+
+        // 安装工具
+        async installTool(toolKey) {
+            try {
+                // 查找可能存在的按钮元素
+                const btnElement = document.querySelector(`button[data-tool="${toolKey}"]`);
+                let elProgress = null;
+                
+                // 如果是通过按钮点击调用的,获取进度条元素
+                if (btnElement) {
+                    if (btnElement.getAttribute('data-undergoing') === '1') {
+                        return false;
+                    }
+                    btnElement.setAttribute('data-undergoing', '1');
+                    elProgress = btnElement.querySelector('span.x-progress');
+                }
+                
+                // 显示安装进度
+                let pt = 1;
+                await Awesome.install(toolKey);
+                
+                // 只有当进度条元素存在时才更新文本内容
+                if (elProgress) {
+                    elProgress.textContent = `(${pt}%)`;
+                    let ptInterval = setInterval(() => {
+                        elProgress.textContent = `(${pt}%)`;
+                        pt += Math.floor(Math.random() * 20);
+                        if(pt > 100) {
+                            clearInterval(ptInterval);
+                            elProgress.textContent = ``;
+                            
+                            // 在进度条完成后显示安装成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[toolKey].name} 安装成功!`,
+                                type: 'success',
+                                duration: 3000
+                            });
+                        }
+                    }, 100);
+                } else {
+                    // 如果没有进度条元素,直接显示通知
+                    this.showInPageNotification({
+                        message: `${this.originalTools[toolKey].name} 安装成功!`,
+                        type: 'success',
+                        duration: 3000
+                    });
+                }
+                
+                // 更新原始数据和当前活动数据
+                this.originalTools[toolKey].installed = true;
+                if (this.activeTools[toolKey]) {
+                    this.activeTools[toolKey].installed = true;
+                }
+                
+                // 更新已安装工具数量
+                this.updateInstalledCount();
+                
+                // 如果按钮存在,更新其状态
+                if (btnElement) {
+                    btnElement.setAttribute('data-undergoing', '0');
+                }
+                
+                // 发送消息通知后台更新
+                chrome.runtime.sendMessage({
+                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                    toolName: toolKey,
+                    action: 'install',
+                    showTips: true
+                });
+                
+            } catch (error) {
+                console.error('安装工具失败:', error);
+                
+                // 显示安装失败的通知
+                this.showInPageNotification({
+                    message: `安装失败:${error.message || '未知错误'}`,
+                    type: 'error',
+                    duration: 5000
+                });
+            }
+        },
+
+        // 卸载工具
+        async uninstallTool(toolKey) {
+            try {
+                // 使用自定义确认对话框而非浏览器原生的confirm
+                this.showConfirm({
+                    title: '卸载确认',
+                    message: `确定要卸载"${this.originalTools[toolKey].name}"工具吗?`,
+                    callback: async (key) => {
+                        try {
+                            await chrome.runtime.sendMessage({
+                                type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                                toolName: key,
+                                action: 'offload',
+                                showTips: true
+                            });
+                            
+                            // 调用Awesome.offLoad卸载工具
+                            await Awesome.offLoad(key);
+                            
+                            // 更新原始数据和当前活动数据
+                            this.originalTools[key].installed = false;
+                            this.originalTools[key].inContextMenu = false;
+                            
+                            if (this.activeTools[key]) {
+                                this.activeTools[key].installed = false;
+                                this.activeTools[key].inContextMenu = false;
+                            }
+                            
+                            // 更新已安装工具数量
+                            this.updateInstalledCount();
+                            
+                            // 显示卸载成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[key].name} 已成功卸载!`,
+                                type: 'success',
+                                duration: 3000
+                            });
+                        } catch (error) {
+                            console.error('卸载工具失败:', error);
+                            
+                            // 显示卸载失败的通知
+                            this.showInPageNotification({
+                                message: `卸载失败:${error.message || '未知错误'}`,
+                                type: 'error',
+                                duration: 5000
+                            });
+                        }
+                    },
+                    data: toolKey
+                });
+            } catch (error) {
+                console.error('准备卸载过程中出错:', error);
+            }
+        },
+
+        // 切换右键菜单
+        async toggleContextMenu(toolKey) {
+            try {
+                const tool = this.originalTools[toolKey];
+                const newState = !tool.inContextMenu;
+                
+                // 更新菜单状态
+                await Awesome.menuMgr(toolKey, newState ? 'install' : 'offload');
+                
+                // 更新原始数据和当前活动数据
+                tool.inContextMenu = newState;
+                if (this.activeTools[toolKey]) {
+                    this.activeTools[toolKey].inContextMenu = newState;
+                }
+                
+                // 发送消息通知后台更新右键菜单
+                chrome.runtime.sendMessage({
+                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                    action: `menu-${newState ? 'install' : 'offload'}`,
+                    showTips: false,
+                    menuOnly: true
+                });
+            } catch (error) {
+                console.error('切换右键菜单失败:', error);
+            }
+        },
+
+        // 切换收藏状态
+        async toggleFavorite(toolKey) {
+            try {
+                if (this.favorites.has(toolKey)) {
+                    this.favorites.delete(toolKey);
+                    // 更新原始数据和当前活动数据
+                    this.originalTools[toolKey].favorite = false;
+                    if (this.activeTools[toolKey]) {
+                        this.activeTools[toolKey].favorite = false;
+                    }
+                } else {
+                    this.favorites.add(toolKey);
+                    // 更新原始数据和当前活动数据
+                    this.originalTools[toolKey].favorite = true;
+                    if (this.activeTools[toolKey]) {
+                        this.activeTools[toolKey].favorite = true;
+                    }
+                }
+                await this.saveFavorites();
+                
+                // 如果是在收藏视图,需要更新视图
+                if (this.currentView === 'favorites') {
+                    this.updateActiveTools('favorites');
+                }
+            } catch (error) {
+                console.error('切换收藏状态失败:', error);
+            }
+        },
+
+        async updateActiveTools(view) {
+            if (this.loading || Object.keys(this.originalTools).length === 0) {
+                return;
+            }
+
+            switch (view) {
+                case 'installed':
+                    // 使用Awesome.getInstalledTools实时获取已安装工具
+                    try {
+                        const installedTools = await Awesome.getInstalledTools();
+                        // 合并installedTools与originalTools的数据
+                        this.activeTools = Object.fromEntries(
+                            Object.entries(this.originalTools).filter(([key]) => 
+                                installedTools.hasOwnProperty(key)
+                            )
+                        );
+                    } catch (error) {
+                        console.error('获取已安装工具失败:', error);
+                        // 回退到本地数据
+                        this.activeTools = Object.fromEntries(
+                            Object.entries(this.originalTools).filter(([_, tool]) => 
+                                tool.installed || tool.systemInstalled || false
+                            )
+                        );
+                    }
+                    break;
+                case 'favorites':
+                    this.activeTools = Object.fromEntries(
+                        Object.entries(this.originalTools).filter(([key]) => this.favorites.has(key))
+                    );
+                    break;
+                case 'recent':
+                    // 切换recent时,recentUsed已在showRecentUsed中实时拉取
+                    this.activeTools = Object.fromEntries(
+                        Object.entries(this.originalTools).filter(([key]) => this.recentUsed.includes(key))
+                    );
+                    break;
+                case 'all':
+                default:
+                    this.activeTools = { ...this.originalTools };
+                    // 分类过滤在computed属性中处理
+                    break;
+            }
+        },
+
+        // 新增更新已安装工具数量的方法
+        async updateInstalledCount() {
+            this.installedCount = await this.getInstalledCount();
+        },
+
+        // 加载用户保存的视图模式
+        async loadViewMode() {
+            try {
+                const result = await new Promise(resolve => {
+                    chrome.storage.local.get('fehelper_view_mode', result => {
+                        resolve(result.fehelper_view_mode);
+                    });
+                });
+                
+                if (result) {
+                    this.viewMode = result;
+                }
+            } catch (error) {
+                console.error('加载视图模式失败:', error);
+            }
+        },
+
+        // 保存用户的视图模式选择
+        async saveViewMode(mode) {
+            try {
+                this.viewMode = mode;
+                await chrome.storage.local.set({
+                    'fehelper_view_mode': mode
+                });
+            } catch (error) {
+                console.error('保存视图模式失败:', error);
+            }
+        },
+
+        // 加载设置项
+        async loadSettings() {
+            try {
+                Settings.getOptions(async (opts) => {
+                    let selectedOpts = [];
+                    Object.keys(opts).forEach(key => {
+                        if(String(opts[key]) === 'true') {
+                            selectedOpts.push(key);
+                        }
+                    });
+                    this.selectedOpts = selectedOpts;
+                    
+                    // 加载右键菜单设置
+                    this.menuDownloadCrx = await Awesome.menuMgr('download-crx', 'get') === '1';
+                    this.menuFeHelperSeting = await Awesome.menuMgr('fehelper-setting', 'get') !== '0';
+                    
+                    // 获取快捷键
+                    chrome.commands.getAll((commands) => {
+                        for (let command of commands) {
+                            if (command.name === '_execute_action') {
+                                this.defaultKey = command.shortcut || 'Alt+Shift+J';
+                                break;
+                            }
+                        }
+                    });
+                });
+            } catch (error) {
+                console.error('加载设置项失败:', error);
+            }
+        },
+        
+        // 检查浏览器类型
+        checkBrowserType() {
+            try {
+                this.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
+            } catch (error) {
+                console.error('检查浏览器类型失败:', error);
+                this.isFirefox = false;
+            }
+        },
+        
+        // 显示设置模态框
+        showSettings() {
+            this.showSettingsModal = true;
+        },
+
+        // 关闭设置模态框
+        closeSettings() {
+            this.showSettingsModal = false;
+        },
+
+        // 显示打赏模态框
+        openDonateModal() {
+            this.showDonateModal = true;
+        },
+
+        // 关闭打赏模态框
+        closeDonateModal() {
+            this.showDonateModal = false;
+        },
+
+        // 显示确认对话框
+        showConfirm(options) {
+            this.confirmDialog = {
+                show: true,
+                title: options.title || '操作确认',
+                message: options.message || '确定要执行此操作吗?',
+                callback: options.callback || null,
+                data: options.data || null
+            };
+        },
+
+        // 确认操作
+        confirmAction() {
+            if (this.confirmDialog.callback) {
+                this.confirmDialog.callback(this.confirmDialog.data);
+            }
+            this.confirmDialog.show = false;
+        },
+
+        // 取消确认
+        cancelConfirm() {
+            this.confirmDialog.show = false;
+        },
+
+        // 保存设置
+        async saveSettings() {
+            try {
+                // 构建设置对象
+                let opts = {};
+                ['OPT_ITEM_CONTEXTMENUS', 'FORBID_OPEN_IN_NEW_TAB', 'CONTENT_SCRIPT_ALLOW_ALL_FRAMES', 
+                 'JSON_PAGE_FORMAT', 'AUTO_DARK_MODE', 'ALWAYS_DARK_MODE'].forEach(key => {
+                    opts[key] = this.selectedOpts.includes(key).toString();
+                });
+                
+                // 保存设置 - 直接传递对象,settings.js已增加对对象类型的支持
+                Settings.setOptions(opts, async () => {
+                    try {
+                        // 处理右键菜单
+                        const crxAction = this.menuDownloadCrx ? 'install' : 'offload';
+                        const settingAction = this.menuFeHelperSeting ? 'install' : 'offload';
+                        
+                        await Promise.all([
+                            Awesome.menuMgr('download-crx', crxAction),
+                            Awesome.menuMgr('fehelper-setting', settingAction)
+                        ]);
+                        
+                        // 通知后台更新右键菜单
+                        chrome.runtime.sendMessage({
+                            type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                            action: 'menu-change',
+                            menuOnly: true
+                        });
+                        
+                        // 关闭弹窗
+                        this.closeSettings();
+                        
+                        // 显示提示
+                        this.showNotification({
+                            title: 'FeHelper 设置',
+                            message: '设置已保存!'
+                        });
+                    } catch (innerError) {
+                        console.error('保存菜单设置失败:', innerError);
+                        this.showNotification({
+                            title: 'FeHelper 设置错误',
+                            message: '保存菜单设置失败: ' + innerError.message
+                        });
+                    }
+                });
+            } catch (error) {
+                console.error('保存设置失败:', error);
+                this.showNotification({
+                    title: 'FeHelper 设置错误',
+                    message: '保存设置失败: ' + error.message
+                });
+            }
+        },
+        
+        // 设置快捷键
+        setShortcuts() {
+            chrome.tabs.create({
+                url: 'chrome://extensions/shortcuts'
+            });
+        },
+        
+        // 体验夜间模式
+        turnLight(event) {
+            event.preventDefault();
+            
+            // 获取body元素
+            const body = document.body;
+            
+            // 切换夜间模式
+            if (body.classList.contains('dark-mode')) {
+                body.classList.remove('dark-mode');
+            } else {
+                body.classList.add('dark-mode');
+                
+                // 设置倒计时
+                this.countDown = 10;
+                
+                // 启动倒计时
+                const timer = setInterval(() => {
+                    this.countDown--;
+                    if (this.countDown <= 0) {
+                        clearInterval(timer);
+                        body.classList.remove('dark-mode');
+                    }
+                }, 1000);
+            }
+        },
+
+        // 检查URL中的donate_from参数并显示打赏弹窗
+        checkDonateParam() {
+            try {
+                const urlParams = new URLSearchParams(window.location.search);
+                const donateFrom = urlParams.get('donate_from');
+                
+                if (donateFrom) {
+                    console.log('检测到打赏来源参数:', donateFrom);
+                    
+                    // 记录打赏来源
+                    chrome.storage.local.set({
+                        'fehelper_donate_from': donateFrom,
+                        'fehelper_donate_time': Date.now()
+                    });
+                    
+                    // 等待工具数据加载完成
+                    this.$nextTick(() => {
+                        // 在所有工具中查找匹配项
+                        let matchedTool = null;
+                        
+                        // 首先尝试直接匹配工具key
+                        if (this.originalTools && this.originalTools[donateFrom]) {
+                            matchedTool = this.originalTools[donateFrom];
+                        } else if (this.originalTools) {
+                            // 如果没有直接匹配,尝试在所有工具中查找部分匹配
+                            for (const [key, tool] of Object.entries(this.originalTools)) {
+                                if (key.includes(donateFrom) || donateFrom.includes(key) ||
+                                    (tool.name && tool.name.includes(donateFrom)) || 
+                                    (donateFrom && donateFrom.includes(tool.name))) {
+                                    matchedTool = tool;
+                                    break;
+                                }
+                            }
+                        }
+                        
+                        // 更新打赏文案
+                        if (matchedTool) {
+                            this.donate.text = `看起来【${matchedTool.name}】工具帮助到了你,感谢你的认可!`;
+                        } else {
+                            // 没有匹配到特定工具,使用通用文案
+                            this.donate.text = `感谢你对FeHelper的认可和支持!`;
+                        }
+                        
+                        // 显示打赏弹窗
+                        this.showDonateModal = true;
+                    });
+                }
+            } catch (error) {
+                console.error('处理打赏参数时出错:', error);
+            }
+        },
+
+        // 补充 getRecentCount,保证模板调用不报错,且数据源唯一
+        async getRecentCount() {
+            const recent = await Statistics.getRecentUsedTools(10);
+            return recent.length;
+        },
+
+        renderDashboard() {
+            const dashboardContainerId = 'fh-dashboard-panel';
+            let container = document.getElementById(dashboardContainerId);
+            // 只在showDashboard且currentView为recent时隐藏工具列表
+            const grid = document.querySelector('.tools-grid');
+            if (!this.showDashboard || this.currentView !== 'recent') {
+                if (container) container.style.display = 'none';
+                if (grid) grid.style.display = '';
+                return;
+            }
+            if (grid) grid.style.display = 'none';
+            if (!container) {
+                container = document.createElement('div');
+                container.id = dashboardContainerId;
+                container.style = 'padding:32px; background:#fff; border-radius:8px; margin:24px; box-shadow:0 2px 12px #eee; min-width:700px;';
+                const main = document.querySelector('.market-main') || document.querySelector('.market-content');
+                if (main) main.prepend(container);
+                else document.body.appendChild(container);
+            }
+            container.style.display = 'block';
+            const data = this.dashboardData || {};
+            // 工具ID转中文名和icon
+            const toolName = (key) => (this.originalTools && this.originalTools[key] && this.originalTools[key].name) ? this.originalTools[key].name : key;
+            const toolIcon = (key) => {
+                if (toolMap[key] && toolMap[key].menuConfig && toolMap[key].menuConfig[0] && toolMap[key].menuConfig[0].icon) {
+                    return toolMap[key].menuConfig[0].icon;
+                }
+                return toolName(key).slice(0,1);
+            };
+            // 插入美观样式
+            if (!document.getElementById('fh-dashboard-style')) {
+                const style = document.createElement('style');
+                style.id = 'fh-dashboard-style';
+                style.innerHTML = `
+                .fh-dashboard-cards { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 24px;}
+                .fh-card { background: linear-gradient(135deg,#f7f9fa 60%,#e3eafc 100%); border-radius: 12px; box-shadow:0 2px 8px #f0f0f0; padding:18px 24px; min-width:120px; flex:1; text-align:center; font-size:15px;}
+                .fh-card.main { background: linear-gradient(135deg,#e3fcec 60%,#e3eafc 100%);}
+                .fh-card-num { font-size:32px; font-weight:bold; margin-bottom:4px;}
+                .fh-calendar { display:inline-block; margin-left:12px; }
+                .fh-cal-cell { display:inline-block; width:18px; height:18px; line-height:18px; text-align:center; border-radius:3px; margin:1px; background:#eee; color:#888; font-size:12px;}
+                .fh-cal-cell.used { background:#4285f4; color:#fff; font-weight:bold;}
+                .fh-dashboard-section { background:#fff; border-radius:12px; box-shadow:0 1px 4px #f0f0f0; padding:18px 24px; margin-bottom:24px;}
+                .fh-dashboard-header { margin-bottom:24px; }
+                .fh-dashboard-header h2 { font-size:22px; margin:0; }
+                .fh-tool-bar { display:inline-block; width:18px; height:18px; border-radius:3px; background:#e3eafc; margin-right:6px; vertical-align:middle; }
+                .fh-tool-bar-inner { display:inline-block; height:100%; border-radius:3px; background:#4285f4; }
+                .fh-tool-list { margin:0; padding:0; list-style:none; }
+                .fh-tool-list li { margin-bottom:10px; }
+                .fh-tool-icon { display:inline-block; width:18px; height:18px; border-radius:3px; background:#e3eafc; margin-right:6px; vertical-align:middle; text-align:center; font-size:14px; }
+                .fh-dashboard-sub { color:#888; font-size:13px; margin-bottom:8px; }
+                `;
+                document.head.appendChild(style);
+            }
+            // 30天活跃日历
+            const today = new Date();
+            let calendar = '<div class="fh-calendar">';
+            for(let i=29;i>=0;i--){
+                const d = new Date(today.getTime()-i*86400000);
+                const ds = d.toISOString().slice(0,10);
+                const used = data.allDates && data.allDates.includes(ds);
+                calendar += `<span class="fh-cal-cell${used?' used':''}" title="${ds}">${d.getDate()}</span>`;
+            }
+            calendar += '</div>';
+            // 主卡片区块
+            let html = `
+            <div class="fh-dashboard-header">
+              <h2>FeHelper 使用统计仪表盘 <span style="font-size:16px;color:#bbb;">(近30天)</span></h2>
+            </div>
+            <div class="fh-dashboard-cards">
+              <div class="fh-card main"><div class="fh-card-num">${data.totalCount||0}</div><div>总使用次数</div></div>
+              <div class="fh-card main"><div class="fh-card-num">${data.activeDays||0}</div><div>活跃天数</div></div>
+              <div class="fh-card"><div>${data.firstDate||'-'}<br>~<br>${data.lastDate||'-'}</div><div>统计区间</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.maxStreak||0}</div><div>最长连续活跃天数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.monthCount||0}</div><div>本月使用次数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.weekCount||0}</div><div>本周使用次数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.avgPerDay||0}</div><div>平均每日使用</div></div>
+              <div class="fh-card"><div>${data.maxDay.date||'-'}<br><b>${data.maxDay.count||0}</b></div><div>最活跃日</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.daysSinceLast||0}</div><div>最近未使用天数</div></div>
+            </div>
+            <div class="fh-dashboard-section">
+                <div class="fh-dashboard-sub">近30天活跃日历:</div>${calendar}
+            </div>
+            <div class="fh-dashboard-section" style="display:flex;gap:32px;flex-wrap:wrap;">
+                <div style="flex:2;min-width:320px;">
+                    <div class="fh-dashboard-sub"><b>最近10天活跃趋势:</b></div>
+                    <div style="display:flex;align-items:end;height:80px;margin-top:8px;">
+                        ${
+                            (data.dailyTrend||[]).map(d=>{
+                                const max = Math.max(...(data.dailyTrend||[]).map(x=>x.count),1);
+                                return `<div title='${d.date}: ${d.count}' style='width:20px;height:${d.count/max*60}px;background:#4285f4;margin-right:4px;border-radius:2px;'></div>`;
+                            }).join('')
+                        }
+                    </div>
+                    <div style="font-size:12px;color:#888;margin-top:4px;">
+                        ${(data.dailyTrend||[]).map(d=>`<span style='display:inline-block;width:20px;text-align:center;'>${d.date.slice(5)}</span>`).join('')}
+                    </div>
+                </div>
+                <div style="flex:3;min-width:320px;">
+                    <div class="fh-dashboard-sub"><b>使用最多的工具:</b></div>
+                    <ul class="fh-tool-list">
+                        ${(data.mostUsed||[]).map(t=>{
+                            const percent = data.totalCount ? Math.round(t.count/data.totalCount*100) : 0;
+                            return `<li style='margin-bottom:12px;display:flex;align-items:center;'>
+                                <span class='fh-tool-icon'>${toolIcon(t.name)}</span>
+                                <span style='display:inline-block;width:100px;'>${toolName(t.name)}</span>
+                                <span style='display:inline-block;width:60px;color:#888;'>(x${t.count})</span>
+                                <span class='fh-tool-bar' style='width:80px;height:10px;margin:0 8px;'>
+                                    <span class='fh-tool-bar-inner' style='width:${percent*0.8}px;'></span>
+                                </span>
+                                <span style='color:#888;'>${percent}%</span>
+                            </li>`;
+                        }).join('')}
+                    </ul>
+                </div>
+            </div>
+            <div class="fh-dashboard-section">
+                <div class="fh-dashboard-sub"><b>最近10次使用的工具:</b></div>
+                <ul style="margin:8px 0 0 0;padding:0;list-style:none;">
+                    ${(data.recentDetail||[]).map(t=>`<li style='display:inline-block;margin-right:24px;'>${toolName(t.tool)} <span style='color:#888;'>(${t.date})</span></li>`).join('')}
+                </ul>
+            </div>
+            `;
+            container.innerHTML = html;
+            window.__vue__ = this;
+        },
+    },
+
+    watch: {
+        // 监听currentView变化
+        currentView: {
+            immediate: true,
+            handler(newView) {
+                this.updateActiveTools(newView);
+            }
+        },
+        
+        // 监听currentCategory变化
+        currentCategory: {
+            handler(newCategory) {
+                // 保证在视图模式之外的分类切换也能正确显示
+                if (this.currentView === 'all') {
+                    this.activeTools = { ...this.originalTools };
+                }
+                // 重置搜索条件
+                if (this.searchKey) {
+                    this.searchKey = '';
+                }
+            }
+        },
+        showDashboard(val) {
+            this.renderDashboard();
+        },
+        dashboardData(val) {
+            this.renderDashboard();
+        },
+    },
+
+    mounted() {
+        this.$nextTick(() => {
+            this.renderDashboard();
+        });
+    },
+});
+
+// 添加滚动事件监听
+window.addEventListener('scroll', () => {
+    const header = document.querySelector('.market-header');
+    const sidebar = document.querySelector('.market-sidebar');
+    
+    if (window.scrollY > 10) {
+        header.classList.add('scrolled');
+        sidebar && sidebar.classList.add('scrolled');
+    } else {
+        header.classList.remove('scrolled');
+        sidebar && sidebar.classList.remove('scrolled');
+    }
+});
+
+// 页面加载后自动采集
+if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
+    Awesome.collectAndSendClientInfo();
+} 

+ 172 - 42
apps/options/market.js

@@ -1,6 +1,8 @@
 import Awesome from '../background/awesome.js'
 import MSG_TYPE from '../static/js/common.js';
 import Settings from './settings.js';
+import Statistics from '../background/statistics.js';
+import toolMap from '../background/tools.js';
 
 // 工具分类定义
 const TOOL_CATEGORIES = [
@@ -58,10 +60,15 @@ new Vue({
             callback: null,
             data: null
         },
+
+        recentCount: 0,
+        showDashboard: false, // 是否显示DashBoard
+        dashboardData: null, // DashBoard数据
     },
 
     async created() {
         await this.initData();
+        this.recentCount = (await Statistics.getRecentUsedTools(10)).length;
         // 初始化后更新已安装工具数量
         this.updateInstalledCount();
         // 恢复用户的视图模式设置
@@ -157,6 +164,7 @@ new Vue({
                 // 获取最近使用数据
                 const recentUsed = await this.getRecentUsedData();
                 this.recentUsed = recentUsed;
+                this.recentCount = recentUsed.length;
 
                 // 获取已安装工具列表
                 const installedTools = await Awesome.getInstalledTools();
@@ -576,11 +584,8 @@ new Vue({
         },
 
         async getRecentUsedData() {
-            return new Promise((resolve) => {
-                chrome.storage.local.get('recentUsed', (result) => {
-                    resolve(result.recentUsed || []);
-                });
-            });
+            // 直接从Statistics模块获取最近使用的工具
+            return await Statistics.getRecentUsedTools(10);
         },
 
         async saveFavorites() {
@@ -597,12 +602,6 @@ new Vue({
             }
         },
 
-        async saveRecentUsed() {
-            await chrome.storage.local.set({
-                recentUsed: this.recentUsed
-            });
-        },
-
         handleSearch() {
             // 搜索时不重置视图类型,允许在已过滤的结果中搜索
         },
@@ -613,12 +612,11 @@ new Vue({
                 this.currentView = 'all';
                 this.updateActiveTools('all');
             }
-            
             this.currentCategory = category;
             this.searchKey = '';
-            
             // 确保工具显示正确
             this.activeTools = { ...this.originalTools };
+            this.showDashboard = false;
         },
 
         handleSort() {
@@ -649,10 +647,6 @@ new Vue({
             return this.favorites.size;
         },
 
-        getRecentCount() {
-            return this.recentUsed.length;
-        },
-
         getToolCategory(toolKey) {
             for (const category of TOOL_CATEGORIES) {
                 if (category.tools.includes(toolKey)) {
@@ -669,6 +663,7 @@ new Vue({
             await this.updateActiveTools('installed');
             // 更新已安装工具数量
             await this.updateInstalledCount();
+            this.showDashboard = false;
         },
 
         showMyFavorites() {
@@ -676,13 +671,24 @@ new Vue({
             this.currentCategory = '';
             this.searchKey = '';
             this.updateActiveTools('favorites');
+            this.showDashboard = false;
         },
 
-        showRecentUsed() {
+        async showRecentUsed() {
             this.currentView = 'recent';
             this.currentCategory = '';
             this.searchKey = '';
-            this.updateActiveTools('recent');
+            // 拉取DashBoard数据并显示
+            this.dashboardData = await Statistics.getDashboardData();
+            this.showDashboard = true;
+            // 不再更新工具列表
+        },
+
+        // 关闭DashBoard,恢复工具列表
+        closeDashboard() {
+            this.showDashboard = false;
+            this.currentView = 'all';
+            this.updateActiveTools('all');
         },
 
         // 重置工具列表到原始状态
@@ -743,9 +749,6 @@ new Vue({
                     this.activeTools[toolKey].installed = true;
                 }
                 
-                // 添加到最近使用
-                this.addToRecentUsed(toolKey);
-                
                 // 更新已安装工具数量
                 this.updateInstalledCount();
                 
@@ -885,24 +888,6 @@ new Vue({
             }
         },
 
-        addToRecentUsed(toolKey) {
-            // 移除已存在的记录
-            const index = this.recentUsed.indexOf(toolKey);
-            if (index > -1) {
-                this.recentUsed.splice(index, 1);
-            }
-            
-            // 添加到开头
-            this.recentUsed.unshift(toolKey);
-            
-            // 限制最大记录数
-            if (this.recentUsed.length > 10) {
-                this.recentUsed.pop();
-            }
-            
-            this.saveRecentUsed();
-        },
-
         async updateActiveTools(view) {
             if (this.loading || Object.keys(this.originalTools).length === 0) {
                 return;
@@ -935,6 +920,7 @@ new Vue({
                     );
                     break;
                 case 'recent':
+                    // 切换recent时,recentUsed已在showRecentUsed中实时拉取
                     this.activeTools = Object.fromEntries(
                         Object.entries(this.originalTools).filter(([key]) => this.recentUsed.includes(key))
                     );
@@ -1205,6 +1191,138 @@ new Vue({
                 console.error('处理打赏参数时出错:', error);
             }
         },
+
+        // 补充 getRecentCount,保证模板调用不报错,且数据源唯一
+        async getRecentCount() {
+            const recent = await Statistics.getRecentUsedTools(10);
+            return recent.length;
+        },
+
+        renderDashboard() {
+            const dashboardContainerId = 'fh-dashboard-panel';
+            let container = document.getElementById(dashboardContainerId);
+            // 只在showDashboard且currentView为recent时隐藏工具列表
+            const grid = document.querySelector('.tools-grid');
+            if (!this.showDashboard || this.currentView !== 'recent') {
+                if (container) container.style.display = 'none';
+                if (grid) grid.style.display = '';
+                return;
+            }
+            if (grid) grid.style.display = 'none';
+            if (!container) {
+                container = document.createElement('div');
+                container.id = dashboardContainerId;
+                container.style = 'padding:32px; background:#fff; border-radius:8px; margin:24px; box-shadow:0 2px 12px #eee; min-width:700px;';
+                const main = document.querySelector('.market-main') || document.querySelector('.market-content');
+                if (main) main.prepend(container);
+                else document.body.appendChild(container);
+            }
+            container.style.display = 'block';
+            const data = this.dashboardData || {};
+            // 工具ID转中文名和icon
+            const toolName = (key) => (this.originalTools && this.originalTools[key] && this.originalTools[key].name) ? this.originalTools[key].name : key;
+            const toolIcon = (key) => {
+                if (toolMap[key] && toolMap[key].menuConfig && toolMap[key].menuConfig[0] && toolMap[key].menuConfig[0].icon) {
+                    return toolMap[key].menuConfig[0].icon;
+                }
+                return toolName(key).slice(0,1);
+            };
+            // 插入美观样式
+            if (!document.getElementById('fh-dashboard-style')) {
+                const style = document.createElement('style');
+                style.id = 'fh-dashboard-style';
+                style.innerHTML = `
+                .fh-dashboard-cards { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 24px;}
+                .fh-card { background: linear-gradient(135deg,#f7f9fa 60%,#e3eafc 100%); border-radius: 12px; box-shadow:0 2px 8px #f0f0f0; padding:18px 24px; min-width:120px; flex:1; text-align:center; font-size:15px;}
+                .fh-card.main { background: linear-gradient(135deg,#e3fcec 60%,#e3eafc 100%);}
+                .fh-card-num { font-size:32px; font-weight:bold; margin-bottom:4px;}
+                .fh-calendar { display:inline-block; margin-left:12px; }
+                .fh-cal-cell { display:inline-block; width:18px; height:18px; line-height:18px; text-align:center; border-radius:3px; margin:1px; background:#eee; color:#888; font-size:12px;}
+                .fh-cal-cell.used { background:#4285f4; color:#fff; font-weight:bold;}
+                .fh-dashboard-section { background:#fff; border-radius:12px; box-shadow:0 1px 4px #f0f0f0; padding:18px 24px; margin-bottom:24px;}
+                .fh-dashboard-header { margin-bottom:24px; }
+                .fh-dashboard-header h2 { font-size:22px; margin:0; }
+                .fh-tool-bar { display:inline-block; width:18px; height:18px; border-radius:3px; background:#e3eafc; margin-right:6px; vertical-align:middle; }
+                .fh-tool-bar-inner { display:inline-block; height:100%; border-radius:3px; background:#4285f4; }
+                .fh-tool-list { margin:0; padding:0; list-style:none; }
+                .fh-tool-list li { margin-bottom:10px; }
+                .fh-tool-icon { display:inline-block; width:18px; height:18px; border-radius:3px; background:#e3eafc; margin-right:6px; vertical-align:middle; text-align:center; font-size:14px; }
+                .fh-dashboard-sub { color:#888; font-size:13px; margin-bottom:8px; }
+                `;
+                document.head.appendChild(style);
+            }
+            // 30天活跃日历
+            const today = new Date();
+            let calendar = '<div class="fh-calendar">';
+            for(let i=29;i>=0;i--){
+                const d = new Date(today.getTime()-i*86400000);
+                const ds = d.toISOString().slice(0,10);
+                const used = data.allDates && data.allDates.includes(ds);
+                calendar += `<span class="fh-cal-cell${used?' used':''}" title="${ds}">${d.getDate()}</span>`;
+            }
+            calendar += '</div>';
+            // 主卡片区块
+            let html = `
+            <div class="fh-dashboard-header">
+              <h2>FeHelper 使用统计仪表盘 <span style="font-size:16px;color:#bbb;">(近30天)</span></h2>
+            </div>
+            <div class="fh-dashboard-cards">
+              <div class="fh-card main"><div class="fh-card-num">${data.totalCount||0}</div><div>总使用次数</div></div>
+              <div class="fh-card main"><div class="fh-card-num">${data.activeDays||0}</div><div>活跃天数</div></div>
+              <div class="fh-card"><div>${data.firstDate||'-'}<br>~<br>${data.lastDate||'-'}</div><div>统计区间</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.maxStreak||0}</div><div>最长连续活跃天数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.monthCount||0}</div><div>本月使用次数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.weekCount||0}</div><div>本周使用次数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.avgPerDay||0}</div><div>平均每日使用</div></div>
+              <div class="fh-card"><div>${data.maxDay.date||'-'}<br><b>${data.maxDay.count||0}</b></div><div>最活跃日</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.daysSinceLast||0}</div><div>最近未使用天数</div></div>
+            </div>
+            <div class="fh-dashboard-section">
+                <div class="fh-dashboard-sub">近30天活跃日历:</div>${calendar}
+            </div>
+            <div class="fh-dashboard-section" style="display:flex;gap:32px;flex-wrap:wrap;">
+                <div style="flex:2;min-width:320px;">
+                    <div class="fh-dashboard-sub"><b>最近10天活跃趋势:</b></div>
+                    <div style="display:flex;align-items:end;height:80px;margin-top:8px;">
+                        ${
+                            (data.dailyTrend||[]).map(d=>{
+                                const max = Math.max(...(data.dailyTrend||[]).map(x=>x.count),1);
+                                return `<div title='${d.date}: ${d.count}' style='width:20px;height:${d.count/max*60}px;background:#4285f4;margin-right:4px;border-radius:2px;'></div>`;
+                            }).join('')
+                        }
+                    </div>
+                    <div style="font-size:12px;color:#888;margin-top:4px;">
+                        ${(data.dailyTrend||[]).map(d=>`<span style='display:inline-block;width:20px;text-align:center;'>${d.date.slice(5)}</span>`).join('')}
+                    </div>
+                </div>
+                <div style="flex:3;min-width:320px;">
+                    <div class="fh-dashboard-sub"><b>使用最多的工具:</b></div>
+                    <ul class="fh-tool-list">
+                        ${(data.mostUsed||[]).map(t=>{
+                            const percent = data.totalCount ? Math.round(t.count/data.totalCount*100) : 0;
+                            return `<li style='margin-bottom:12px;display:flex;align-items:center;'>
+                                <span class='fh-tool-icon'>${toolIcon(t.name)}</span>
+                                <span style='display:inline-block;width:100px;'>${toolName(t.name)}</span>
+                                <span style='display:inline-block;width:60px;color:#888;'>(x${t.count})</span>
+                                <span class='fh-tool-bar' style='width:80px;height:10px;margin:0 8px;'>
+                                    <span class='fh-tool-bar-inner' style='width:${percent*0.8}px;'></span>
+                                </span>
+                                <span style='color:#888;'>${percent}%</span>
+                            </li>`;
+                        }).join('')}
+                    </ul>
+                </div>
+            </div>
+            <div class="fh-dashboard-section">
+                <div class="fh-dashboard-sub"><b>最近10次使用的工具:</b></div>
+                <ul style="margin:8px 0 0 0;padding:0;list-style:none;">
+                    ${(data.recentDetail||[]).map(t=>`<li style='display:inline-block;margin-right:24px;'>${toolName(t.tool)} <span style='color:#888;'>(${t.date})</span></li>`).join('')}
+                </ul>
+            </div>
+            `;
+            container.innerHTML = html;
+            window.__vue__ = this;
+        },
     },
 
     watch: {
@@ -1228,8 +1346,20 @@ new Vue({
                     this.searchKey = '';
                 }
             }
-        }
-    }
+        },
+        showDashboard(val) {
+            this.renderDashboard();
+        },
+        dashboardData(val) {
+            this.renderDashboard();
+        },
+    },
+
+    mounted() {
+        this.$nextTick(() => {
+            this.renderDashboard();
+        });
+    },
 });
 
 // 添加滚动事件监听

+ 15 - 0
apps/popup/index.js

@@ -47,6 +47,16 @@ new Vue({
 
         // 自动开关灯
         DarkModeMgr.turnLightAuto();
+
+        // 记录工具使用
+        // 埋点:自动触发json-format-auto
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'statistics-tool-usage',
+            params: {
+                tool_name: 'popup'
+            }
+        });
     },
 
     mounted: function () {
@@ -97,6 +107,11 @@ new Vue({
                 return false;
             };
         });
+
+        // 页面加载后自动采集
+        if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
+            Awesome.collectAndSendClientInfo();
+        }
     },
 
     methods: {

+ 8 - 0
apps/poster-maker/css/main.css

@@ -677,6 +677,14 @@ body {
 .navbar-brand {
   display: flex;
   align-items: center;
+  position: relative;
+}
+.mod-head-actions{
+  position: absolute;
+  right: 0;
+} 
+.mod-head-actions a:hover{
+  color: #f00;
 }
 
 .brand-link {

+ 6 - 0
apps/poster-maker/index.html

@@ -17,6 +17,12 @@
         <span class="brand-text">FeHelper</span>
         <span class="brand-subtitle">海报快速生成</span>
       </a>
+      <div class="mod-head-actions">
+          <a href="#" id="donateLink" class="nav-item donate-link">
+              <i class="nav-icon">❤</i>
+              <span>打赏鼓励</span>
+          </a>
+      </div>
     </div>
   </div>
 

+ 12 - 0
apps/poster-maker/js/index.js

@@ -90,3 +90,15 @@ function setupPreviewZoom() {
   // 初始化时自动触发重置缩放
   resetZoomBtn.click();
 }
+
+/**
+ * 打赏鼓励
+ */
+document.getElementById('donateLink').addEventListener('click', (event  ) => {
+  event.preventDefault();
+  chrome.runtime.sendMessage({
+    type: 'fh-dynamic-any-thing',
+    thing: 'open-donate-modal',
+    params: { toolName: 'poster-maker' }
+  });
+});

+ 7 - 2
apps/qr-code/index.css

@@ -244,7 +244,7 @@ td .x-panel .x-tips {
 }
 
 /* 保持原有的顶部导航样式 */
-.x-switch {
+.x-switch,.x-donate-link {
     font-size: 14px;
     font-weight: 500;
     color: #2563eb;
@@ -276,7 +276,7 @@ td .x-panel .x-tips {
     transition: transform 0.3s ease;
 }
 
-.x-switch:hover {
+.x-switch:hover,.x-donate-link:hover {
     background-color: #dbeafe;
     color: #1d4ed8;
     text-decoration: none;
@@ -288,6 +288,11 @@ td .x-panel .x-tips {
     transform: rotate(180deg);
     opacity: 1;
 }
+.x-donate-link {
+    margin: 0 10px;
+    font-size: 12px;
+    font-weight: normal;
+}
 
 form {
     display: none;

+ 1 - 0
apps/qr-code/index.html

@@ -19,6 +19,7 @@
                             <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a><span class="title-text-wrapper-text">| 二维码{{qrEncodeMode? '生成':'解码'}}器</span>
                     </span>
                     <span class="x-switch" ref="btnSwitch" @click="trans">切换为{{!qrEncodeMode? ' 二维码生成器 ' : ' 解码/扫码 '}}模式&gt;&gt;</span>
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
                     <a class="x-other-tools" @click="openOptionsPage()"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                 </h3>
             </div>

+ 9 - 0
apps/qr-code/index.js

@@ -298,6 +298,15 @@ new Vue({
                  // Optionally, provide a fallback link or message
                  // window.open('options.html'); // Example fallback
             }
+        },
+
+        openDonateModal: function(event ){
+            event.preventDefault();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'qr-code' }
+            });
         }
     }
 });

+ 13 - 0
apps/svg-converter/index.js

@@ -1256,6 +1256,19 @@ new Vue({
                     dimensions: '解析出错'
                 };
             }
+        },
+
+        /**
+         * 打开打赏弹窗
+         */
+        openDonateModal: function () {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: {
+                    toolName: 'svg-converter'
+                }
+            });
         }
     }
 });

+ 6 - 2
package.json

@@ -4,7 +4,9 @@
   "description": "FeHelper-前端助手",
   "main": "index.js",
   "dependencies": {
-    "crypto": "^1.0.1"
+    "crypto": "^1.0.1",
+    "geoip-lite": "^1.4.10",
+    "ua-parser-js": "^2.0.3"
   },
   "devDependencies": {
     "@babel/core": "^7.26.10",
@@ -20,6 +22,7 @@
     "gulp-uglify-es": "^3.0.0",
     "gulp-uglifycss": "^1.0.9",
     "gulp-zip": "^4.1.0",
+    "nodemon": "^3.1.10",
     "pretty-bytes": "^4.0.2",
     "run-sequence": "^2.2.1",
     "shelljs": "^0.8.1"
@@ -27,7 +30,8 @@
   "scripts": {
     "watch": "gulp watch",
     "build": "gulp",
-    "preinstall": "npx npm-force-resolutions"
+    "preinstall": "npx npm-force-resolutions",
+    "dev": "nodemon server/index.js"
   },
   "repository": {
     "type": "git",