├── .gitignore ├── LICENSE ├── README.md ├── ddfa8ddd05268105eeeaf0a7dc1f012.jpg ├── manifest.json ├── package.json └── src ├── assets ├── icon-128.ico ├── icon-128.png ├── icon-16.ico ├── icon-16.png ├── icon-32.ico ├── icon-32.png ├── icon-48.ico ├── icon-48.png └── icons │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-32.png │ └── icon-48.png ├── components ├── Highlighter.js └── Popup.js ├── lib └── html2canvas.min.js ├── manifest.json ├── scripts ├── background.js ├── content.js └── popup.js └── styles └── content.css /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | 木兰宽松许可证,第2版 3 | 4 | 木兰宽松许可证,第2版 5 | 6 | 2020年1月 http://license.coscl.org.cn/MulanPSL2 7 | 8 | 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: 9 | 10 | 0. 定义 11 | 12 | “软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 13 | 14 | “贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 15 | 16 | “贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 17 | 18 | “法人实体” 是指提交贡献的机构及其“关联实体”。 19 | 20 | “关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是 21 | 指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 22 | 23 | 1. 授予版权许可 24 | 25 | 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可 26 | 以复制、使用、修改、分发其“贡献”,不论修改与否。 27 | 28 | 2. 授予专利许可 29 | 30 | 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定 31 | 撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡 32 | 献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软 33 | 件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“ 34 | 关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或 35 | 其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权 36 | 行动之日终止。 37 | 38 | 3. 无商标许可 39 | 40 | “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定 41 | 的声明义务而必须使用除外。 42 | 43 | 4. 分发限制 44 | 45 | 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“ 46 | 本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 47 | 48 | 5. 免责声明与责任限制 49 | 50 | “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对 51 | 任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于 52 | 何种法律理论,即使其曾被建议有此种损失的可能性。 53 | 54 | 6. 语言 55 | 56 | “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文 57 | 版为准。 58 | 59 | 条款结束 60 | 61 | 如何将木兰宽松许可证,第2版,应用到您的软件 62 | 63 | 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: 64 | 65 | 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; 66 | 67 | 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; 68 | 69 | 3, 请将如下声明文本放入每个源文件的头部注释中。 70 | 71 | Copyright (c) [Year] [name of copyright holder] 72 | [Software Name] is licensed under Mulan PSL v2. 73 | You can use this software according to the terms and conditions of the Mulan 74 | PSL v2. 75 | You may obtain a copy of Mulan PSL v2 at: 76 | http://license.coscl.org.cn/MulanPSL2 77 | THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY 78 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 79 | NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. 80 | See the Mulan PSL v2 for more details. 81 | 82 | Mulan Permissive Software License,Version 2 83 | 84 | Mulan Permissive Software License,Version 2 (Mulan PSL v2) 85 | 86 | January 2020 http://license.coscl.org.cn/MulanPSL2 87 | 88 | Your reproduction, use, modification and distribution of the Software shall 89 | be subject to Mulan PSL v2 (this License) with the following terms and 90 | conditions: 91 | 92 | 0. Definition 93 | 94 | Software means the program and related documents which are licensed under 95 | this License and comprise all Contribution(s). 96 | 97 | Contribution means the copyrightable work licensed by a particular 98 | Contributor under this License. 99 | 100 | Contributor means the Individual or Legal Entity who licenses its 101 | copyrightable work under this License. 102 | 103 | Legal Entity means the entity making a Contribution and all its 104 | Affiliates. 105 | 106 | Affiliates means entities that control, are controlled by, or are under 107 | common control with the acting entity under this License, ‘control’ means 108 | direct or indirect ownership of at least fifty percent (50%) of the voting 109 | power, capital or other securities of controlled or commonly controlled 110 | entity. 111 | 112 | 1. Grant of Copyright License 113 | 114 | Subject to the terms and conditions of this License, each Contributor hereby 115 | grants to you a perpetual, worldwide, royalty-free, non-exclusive, 116 | irrevocable copyright license to reproduce, use, modify, or distribute its 117 | Contribution, with modification or not. 118 | 119 | 2. Grant of Patent License 120 | 121 | Subject to the terms and conditions of this License, each Contributor hereby 122 | grants to you a perpetual, worldwide, royalty-free, non-exclusive, 123 | irrevocable (except for revocation under this Section) patent license to 124 | make, have made, use, offer for sale, sell, import or otherwise transfer its 125 | Contribution, where such patent license is only limited to the patent claims 126 | owned or controlled by such Contributor now or in future which will be 127 | necessarily infringed by its Contribution alone, or by combination of the 128 | Contribution with the Software to which the Contribution was contributed. 129 | The patent license shall not apply to any modification of the Contribution, 130 | and any other combination which includes the Contribution. If you or your 131 | Affiliates directly or indirectly institute patent litigation (including a 132 | cross claim or counterclaim in a litigation) or other patent enforcement 133 | activities against any individual or entity by alleging that the Software or 134 | any Contribution in it infringes patents, then any patent license granted to 135 | you under this License for the Software shall terminate as of the date such 136 | litigation or activity is filed or taken. 137 | 138 | 3. No Trademark License 139 | 140 | No trademark license is granted to use the trade names, trademarks, service 141 | marks, or product names of Contributor, except as required to fulfill notice 142 | requirements in section 4. 143 | 144 | 4. Distribution Restriction 145 | 146 | You may distribute the Software in any medium with or without modification, 147 | whether in source or executable forms, provided that you provide recipients 148 | with a copy of this License and retain copyright, patent, trademark and 149 | disclaimer statements in the Software. 150 | 151 | 5. Disclaimer of Warranty and Limitation of Liability 152 | 153 | THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY 154 | KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR 155 | COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT 156 | LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING 157 | FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO 158 | MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF 159 | THE POSSIBILITY OF SUCH DAMAGES. 160 | 161 | 6. Language 162 | 163 | THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION 164 | AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF 165 | DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION 166 | SHALL PREVAIL. 167 | 168 | END OF THE TERMS AND CONDITIONS 169 | 170 | How to Apply the Mulan Permissive Software License,Version 2 171 | (Mulan PSL v2) to Your Software 172 | 173 | To apply the Mulan PSL v2 to your work, for easy identification by 174 | recipients, you are suggested to complete following three steps: 175 | 176 | i. Fill in the blanks in following statement, including insert your software 177 | name, the year of the first publication of your software, and your name 178 | identified as the copyright owner; 179 | 180 | ii. Create a file named "LICENSE" which contains the whole context of this 181 | License in the first directory of your software package; 182 | 183 | iii. Attach the statement to the appropriate annotated syntax at the 184 | beginning of each source file. 185 | 186 | Copyright (c) [Year] [name of copyright holder] 187 | [Software Name] is licensed under Mulan PSL v2. 188 | You can use this software according to the terms and conditions of the Mulan 189 | PSL v2. 190 | You may obtain a copy of Mulan PSL v2 at: 191 | http://license.coscl.org.cn/MulanPSL2 192 | THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY 193 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 194 | NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. 195 | See the Mulan PSL v2 for more details. 196 | ======= 197 | MIT License 198 | 199 | Copyright (c) 2024 hehehero 200 | 201 | Permission is hereby granted, free of charge, to any person obtaining a copy 202 | of this software and associated documentation files (the "Software"), to deal 203 | in the Software without restriction, including without limitation the rights 204 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 205 | copies of the Software, and to permit persons to whom the Software is 206 | furnished to do so, subject to the following conditions: 207 | 208 | The above copyright notice and this permission notice shall be included in all 209 | copies or substantial portions of the Software. 210 | 211 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 212 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 213 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 214 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 215 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 216 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 217 | SOFTWARE. 218 | >>>>>>> 0355d9cb78660b87c57eb5d53e168738941226b7 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Answer Assistant (AI助手) 2 | 3 | A Chrome browser extension that helps users quickly get AI answers. | 一个帮助用户快速获取AI回答的Chrome浏览器扩展。 4 | 5 | [English](#english) | [中文](#chinese) 6 | 7 | ## English 8 | 9 | ### Features 10 | 11 | - 🖼️ Quick Screenshot: Support intelligent webpage area capture 12 | - 🤖 AI Recognition: Automatically send screenshots to AI assistant 13 | - ⚡ Real-time Response: Get professional AI answers quickly 14 | - 🎯 Convenient Operation: Support shortcuts and right-click menu 15 | - 🎨 Elegant Display: Clear and beautiful conversation interface 16 | 17 | ### Installation 18 | 19 | 1. Download the extension package 20 | 2. Open Chrome browser and go to extensions page (chrome://extensions/) 21 | 3. Enable Developer Mode 22 | 4. Click "Load unpacked" 23 | 5. Select the unzipped extension folder 24 | 25 | ### Usage 26 | 27 | 1. Click the extension icon in browser toolbar 28 | 2. Select webpage area for screenshot 29 | 3. Wait for AI assistant to analyze and respond 30 | 4. View AI answer results 31 | 32 | ### Shortcuts 33 | 34 | - Start Screenshot: Alt + Shift + A 35 | - Open Menu: Right-click extension icon 36 | 37 | ## Chinese 38 | 39 | ### 功能特点 40 | 41 | - 🖼️ 快速截图:支持网页区域智能截图 42 | - 🤖 AI识别:自动将截图发送至AI助手 43 | - ⚡ 实时响应:快速获取AI的专业回答 44 | - 🎯 便捷操作:支持快捷键和右键菜单 45 | - 🎨 优雅展示:清晰美观的对话界面 46 | 47 | ### 安装使用 48 | 49 | 1. 下载插件压缩包 50 | 2. 打开Chrome浏览器,进入扩展程序页面(chrome://extensions/) 51 | 3. 开启开发者模式 52 | 4. 点击"加载已解压的扩展程序" 53 | 5. 选择解压后的插件文件夹 54 | 55 | ### 使用说明 56 | 57 | 1. 点击浏览器工具栏中的插件图标 58 | 2. 选择需要询问的网页区域进行截图 59 | 3. 等待AI助手分析并给出回答 60 | 4. 查看AI回答结果 61 | 62 | ### 快捷键 63 | 64 | - 开始截图:Alt + Shift + A 65 | - 打开菜单:右键点击插件图标 66 | 67 | ## Project Structure | 项目结构 68 | 69 | ``` 70 | ai-answer-assistant/ 71 | ├── manifest.json # Extension configuration file 72 | ├── package.json # Project configuration file 73 | ├── README.md # Project documentation 74 | └── src/ 75 | ├── assets/ 76 | │ └── icons/ # Extension icon resources 77 | │ ├── icon-16.png 78 | │ ├── icon-32.png 79 | │ ├── icon-48.png 80 | │ └── icon-128.png 81 | ├── scripts/ 82 | │ ├── background.js # Background script 83 | │ └── content.js # Content script 84 | └── styles/ 85 | └── content.css # Style file 86 | ``` 87 | 88 | ## Tech Stack | 技术栈 89 | 90 | - JavaScript ES6+ 91 | - Chrome Extension API 92 | - HTML5/CSS3 93 | 94 | ## Version Info | 版本信息 95 | 96 | Current Version | 当前版本:v1.1.0 97 | 98 | ### Changelog | 更新日志 99 | 100 | v1.1.0 (2024-03) 101 | - ✨ Optimize screenshot function stability | 优化截图功能稳定性 102 | - 🎨 Improve AI answer display effect | 改进AI回答展示效果 103 | - 🚀 Enhance overall user experience | 提升整体用户体验 104 | - 🐛 Fix known issues | 修复已知问题 105 | 106 | ## Development | 开发说明 107 | 108 | ### Local Development | 本地开发 109 | 110 | 1. Clone repository | 克隆仓库: 111 | 112 | ```bash 113 | git clone https://github.com/hehehero/ai-answer-assistant.git 114 | ``` 115 | 116 | 2. Load project folder in Chrome extensions page | 在Chrome扩展管理页面加载项目文件夹 117 | 3. Refresh extension after code changes | 修改代码后刷新扩展即可看到效果 118 | 119 | ## Notes | 注意事项 120 | 121 | - Make sure to grant necessary permissions | 使用前请确保已授予插件必要的权限 122 | - Recommended to use latest Chrome version | 建议使用最新版本的Chrome浏览器 123 | - Check network connection if issues occur | 如遇问题请检查网络连接是否正常 124 | 125 | ## License | 许可证 126 | 127 | MIT License 128 | 129 | ## Repository | 项目地址 130 | 131 | - GitHub:[https://github.com/hehehero/ai-answer-assistant](https://github.com/hehehero/ai-answer-assistant) 132 | - Gitee:[https://gitee.com/hehehero/ai-answer-assistant](https://gitee.com/hehehero/ai-answer-assistant) 133 | 134 | ## Contact | 联系方式 135 | 136 | For issues or suggestions, please contact: | 如有问题或建议,请通过以下方式联系: 137 | - GitHub: [@hehehero](https://github.com/hehehero) 138 | - Gitee: [@hehehero](https://gitee.com/hehehero) 139 | 140 | ## Acknowledgments | 致谢 141 | 142 | Thanks to all friends who provided help and suggestions for this project! | 感谢所有为本项目提供帮助和建议的朋友们! 143 | 144 | ### 🎨如果看到这里了,不妨赞赏一下作者再走吧~吃饱了才更有力气搬砖🤖 145 | 146 | ![alt text](ddfa8ddd05268105eeeaf0a7dc1f012.jpg) 147 | -------------------------------------------------------------------------------- /ddfa8ddd05268105eeeaf0a7dc1f012.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/ddfa8ddd05268105eeeaf0a7dc1f012.jpg -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "AI助手", 4 | "short_name": "AI助手", 5 | "version": "1.1.0", 6 | "description": "一个帮助用户快速获取AI回答的Chrome浏览器扩展", 7 | "permissions": [ 8 | "activeTab", 9 | "clipboardRead", 10 | "clipboardWrite", 11 | "scripting", 12 | "tabs", 13 | "cookies", 14 | "windows", 15 | "system.display" 16 | ], 17 | "host_permissions": [ 18 | "https://www.doubao.com/*" 19 | ], 20 | "content_security_policy": { 21 | "extension_pages": "script-src 'self'; object-src 'self'; frame-src https://*.doubao.com/" 22 | }, 23 | "action": { 24 | "default_icon": { 25 | "16": "src/assets/icons/icon-16.png", 26 | "32": "src/assets/icons/icon-32.png", 27 | "48": "src/assets/icons/icon-48.png", 28 | "128": "src/assets/icons/icon-128.png" 29 | } 30 | }, 31 | "icons": { 32 | "16": "src/assets/icons/icon-16.png", 33 | "32": "src/assets/icons/icon-32.png", 34 | "48": "src/assets/icons/icon-48.png", 35 | "128": "src/assets/icons/icon-128.png" 36 | }, 37 | "content_scripts": [ 38 | { 39 | "matches": [""], 40 | "js": ["src/scripts/content.js"], 41 | "css": ["src/styles/content.css"] 42 | } 43 | ], 44 | "background": { 45 | "service_worker": "src/scripts/background.js" 46 | }, 47 | "web_accessible_resources": [{ 48 | "resources": ["src/scripts/iframe-script.js"], 49 | "matches": ["https://www.doubao.com/*"] 50 | }] 51 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-answer-assistant", 3 | "version": "1.1.0", 4 | "description": "一个帮助用户快速获取AI回答的Chrome浏览器扩展", 5 | "scripts": { 6 | "build": "echo \"Add build script here\"", 7 | "test": "echo \"Add test script here\"" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": {} 11 | } -------------------------------------------------------------------------------- /src/assets/icon-128.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-128.ico -------------------------------------------------------------------------------- /src/assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-128.png -------------------------------------------------------------------------------- /src/assets/icon-16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-16.ico -------------------------------------------------------------------------------- /src/assets/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-16.png -------------------------------------------------------------------------------- /src/assets/icon-32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-32.ico -------------------------------------------------------------------------------- /src/assets/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-32.png -------------------------------------------------------------------------------- /src/assets/icon-48.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-48.ico -------------------------------------------------------------------------------- /src/assets/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icon-48.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icons/icon-128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icons/icon-16.png -------------------------------------------------------------------------------- /src/assets/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icons/icon-32.png -------------------------------------------------------------------------------- /src/assets/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehero/ai-answer-assistant/b7c8b562259ca7ff7b8dc66fcf4084745718a7e3/src/assets/icons/icon-48.png -------------------------------------------------------------------------------- /src/components/Highlighter.js: -------------------------------------------------------------------------------- 1 | export class Highlighter { 2 | constructor() { 3 | this.highlightElement = null; 4 | } 5 | 6 | // 创建高亮框 7 | createHighlight() { 8 | const highlight = document.createElement('div'); 9 | highlight.style.position = 'fixed'; 10 | highlight.style.border = '2px solid red'; 11 | highlight.style.backgroundColor = 'rgba(255, 0, 0, 0.1)'; 12 | highlight.style.pointerEvents = 'none'; 13 | highlight.style.zIndex = '10000'; 14 | highlight.style.transition = 'all 0.2s ease-in-out'; 15 | return highlight; 16 | } 17 | 18 | // 显示高亮 19 | show(element) { 20 | if (!this.highlightElement) { 21 | this.highlightElement = this.createHighlight(); 22 | document.body.appendChild(this.highlightElement); 23 | } 24 | 25 | const rect = element.getBoundingClientRect(); 26 | this.highlightElement.style.top = rect.top + 'px'; 27 | this.highlightElement.style.left = rect.left + 'px'; 28 | this.highlightElement.style.width = rect.width + 'px'; 29 | this.highlightElement.style.height = rect.height + 'px'; 30 | this.highlightElement.style.display = 'block'; 31 | } 32 | 33 | // 隐藏高亮 34 | hide() { 35 | if (this.highlightElement) { 36 | this.highlightElement.style.display = 'none'; 37 | } 38 | } 39 | 40 | // 移除高亮 41 | remove() { 42 | if (this.highlightElement) { 43 | this.highlightElement.remove(); 44 | this.highlightElement = null; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/components/Popup.js: -------------------------------------------------------------------------------- 1 | // 在Popup.js中添加检查 2 | let popupInstance = null; 3 | 4 | function showPopup() { 5 | if (popupInstance) { 6 | // 如果已存在实例,就显示它 7 | popupInstance.style.display = 'block'; 8 | return; 9 | } 10 | 11 | // 创建新的popup实例 12 | const popup = document.createElement('div'); 13 | // ... 其他popup创建代码 ... 14 | 15 | popupInstance = popup; 16 | } 17 | 18 | // 监听消息 19 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 20 | if (request.action === 'showPopup') { 21 | showPopup(); 22 | } 23 | }); -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "网页元素截图工具", 4 | "description": "快速截取网页元素并在其他页面中使用", 5 | "version": "1.1", 6 | "permissions": [ 7 | "activeTab", 8 | "scripting", 9 | "storage", 10 | "clipboardWrite", 11 | "tabs" 12 | ], 13 | "host_permissions": [ 14 | "" 15 | ], 16 | "action": { 17 | "default_popup": "popup/popup.html" 18 | }, 19 | "background": { 20 | "service_worker": "scripts/background.js" 21 | }, 22 | "content_scripts": [ 23 | { 24 | "matches": [""], 25 | "css": ["styles/content.css"], 26 | "js": ["scripts/utils.js", "scripts/content.js"] 27 | } 28 | ], 29 | "web_accessible_resources": [{ 30 | "resources": [ 31 | "lib/html2canvas.min.js" 32 | ], 33 | "matches": [""] 34 | }] 35 | } -------------------------------------------------------------------------------- /src/scripts/background.js: -------------------------------------------------------------------------------- 1 | let screenshotData = null; 2 | 3 | // 存储豆包聊天标签页的ID 4 | let chatTabId = null; 5 | 6 | // 添加重试相关的配置 7 | const CONFIG = { 8 | MESSAGE_TIMEOUT: 40000 // 等待AI回复的超时时间(毫秒) 9 | }; 10 | 11 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 12 | if (request.action === 'captureVisibleTab') { 13 | captureAndCrop(request.data) 14 | .then(() => sendResponse({ status: 'success' })) 15 | .catch(error => { 16 | console.error('截图失败:', error); 17 | sendResponse({ status: 'error', error: error.message }); 18 | }); 19 | return true; // 保持消息通道打开 20 | } 21 | }); 22 | 23 | async function captureAndCrop(data) { 24 | try { 25 | // 捕获整个可见区域 26 | const screenshot = await chrome.tabs.captureVisibleTab(null, { 27 | format: 'png' 28 | }); 29 | 30 | // 发送截图数据到内容脚本进行处理 31 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 32 | chrome.scripting.executeScript({ 33 | target: { tabId: tabs[0].id }, 34 | func: processImage, 35 | args: [screenshot, data] 36 | }); 37 | }); 38 | } catch (error) { 39 | console.error('处理截图失败:', error); 40 | throw error; 41 | } 42 | } 43 | 44 | // 在内容脚本中处理图像 45 | function processImage(screenshot, data) { 46 | const img = new Image(); 47 | img.onload = () => { 48 | const canvas = document.createElement('canvas'); 49 | canvas.width = data.rect.width * data.devicePixelRatio; 50 | canvas.height = data.rect.height * data.devicePixelRatio; 51 | const ctx = canvas.getContext('2d'); 52 | 53 | // 裁剪指定区域 54 | ctx.drawImage(img, 55 | data.rect.x * data.devicePixelRatio, 56 | data.rect.y * data.devicePixelRatio, 57 | data.rect.width * data.devicePixelRatio, 58 | data.rect.height * data.devicePixelRatio, 59 | 0, 0, 60 | data.rect.width * data.devicePixelRatio, 61 | data.rect.height * data.devicePixelRatio 62 | ); 63 | 64 | // 转换为 blob 65 | canvas.toBlob(async (blob) => { 66 | try { 67 | // 先清空剪贴板 68 | const emptyItem = new ClipboardItem({ 69 | 'text/plain': new Blob([''], { type: 'text/plain' }) 70 | }); 71 | await navigator.clipboard.write([emptyItem]); 72 | 73 | // 再写入新的图片 74 | const clipboardItem = new ClipboardItem({ 75 | [blob.type]: blob 76 | }); 77 | await navigator.clipboard.write([clipboardItem]); 78 | console.log('✅ 已保存到剪贴板'); 79 | 80 | // 发送成功消息回content script 81 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 82 | chrome.tabs.sendMessage(tabs[0].id, { 83 | action: 'screenshotSaved', 84 | success: true 85 | }); 86 | }); 87 | } catch (err) { 88 | console.error('保存到剪贴板失败:', err); 89 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 90 | chrome.tabs.sendMessage(tabs[0].id, { 91 | action: 'screenshotSaved', 92 | success: false, 93 | error: err.message 94 | }); 95 | }); 96 | } 97 | }, 'image/png'); 98 | }; 99 | img.src = screenshot; 100 | } 101 | 102 | // 添加一个变量来跟踪扩展窗口 103 | let extensionWindowId = null; 104 | 105 | // 修改点击事件处理 106 | chrome.action.onClicked.addListener(async (tab) => { 107 | try { 108 | // 先检查Popup是否已经存在 109 | const [{ result }] = await chrome.scripting.executeScript({ 110 | target: { tabId: tab.id }, 111 | func: () => window.popup !== undefined 112 | }); 113 | 114 | if (!result) { 115 | // 如果Popup不存在,才注入脚本 116 | await chrome.scripting.executeScript({ 117 | target: { tabId: tab.id }, 118 | files: ['src/components/Popup.js'] 119 | }); 120 | } 121 | 122 | // 发送显示消息 123 | chrome.tabs.sendMessage(tab.id, { action: 'showPopup' }); 124 | } catch (error) { 125 | console.error('处理Popup失败:', error); 126 | } 127 | }); 128 | 129 | // 监听窗口关闭事件 130 | chrome.windows.onRemoved.addListener((windowId) => { 131 | if (windowId === extensionWindowId) { 132 | console.log('扩展窗口已关闭,重置windowId'); 133 | extensionWindowId = null; 134 | } 135 | }); 136 | 137 | // 监听窗口焦点变化 138 | chrome.windows.onFocusChanged.addListener((windowId) => { 139 | if (windowId === extensionWindowId) { 140 | console.log('扩展窗口获得焦点'); 141 | } 142 | }); 143 | 144 | // 添加消息监听 145 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 146 | if (request.action === 'startCapture' || request.action === 'startPaste') { 147 | // 转发消息给content script 148 | chrome.tabs.sendMessage(sender.tab.id, request); 149 | } 150 | }); 151 | 152 | chrome.runtime.onMessage.addListener((request, sender) => { 153 | if (request.action === 'openChatTab') { 154 | // 检查是否有已有的聊天标签页 155 | if (chatTabId) { 156 | chrome.tabs.get(chatTabId, async (tab) => { 157 | if (chrome.runtime.lastError) { 158 | createNewChatTab(); 159 | } else { 160 | chrome.tabs.update(chatTabId, { active: true }, () => { 161 | executePasteScript(chatTabId); 162 | }); 163 | } 164 | }); 165 | } else { 166 | createNewChatTab(); 167 | } 168 | } 169 | }); 170 | 171 | // 修改创建新的聊天标签页的函数 172 | function createNewChatTab() { 173 | // 先关闭已存在的标签页 174 | if (chatTabId) { 175 | try { 176 | chrome.tabs.remove(chatTabId); 177 | } catch (error) { 178 | console.log('关闭已存在标签页失败:', error); 179 | } 180 | } 181 | 182 | // 创建一个面板窗口 183 | chrome.windows.create({ 184 | url: 'https://www.doubao.com/chat', 185 | type: 'panel', // 使用panel类型 186 | focused: false, // 不聚焦 187 | width: 800, 188 | height: 600, 189 | top: 0, 190 | left: 0 191 | }, (window) => { 192 | const tab = window.tabs[0]; 193 | chatTabId = tab.id; 194 | console.log('✅ [Background] 创建新标签页:', chatTabId); 195 | 196 | // 等待一小段时间后移动窗口 197 | setTimeout(() => { 198 | // 移动窗口到屏幕外 199 | chrome.windows.update(window.id, { 200 | left: -2000, 201 | focused: false 202 | }); 203 | }, 100); 204 | 205 | // 监听标签页加载完成 206 | chrome.tabs.onUpdated.addListener(function listener(tabId, info) { 207 | if (tabId === chatTabId && info.status === 'complete') { 208 | chrome.tabs.onUpdated.removeListener(listener); 209 | console.log('✅ [Background] 标签页加载完成'); 210 | 211 | // 执行粘贴脚本 212 | executePasteScript(chatTabId); 213 | } 214 | }); 215 | 216 | // 设置超时检查 217 | setTimeout(() => { 218 | chrome.tabs.get(chatTabId, (tab) => { 219 | if (chrome.runtime.lastError || !tab) { 220 | console.log('❌ [Background] 标签页创建失败或已关闭'); 221 | chatTabId = null; 222 | handleFailedResponse(chatTabId, 0); 223 | } 224 | }); 225 | }, CONFIG.MESSAGE_TIMEOUT); 226 | }); 227 | } 228 | 229 | // 处理消息超时 230 | function handleMessageTimeout(tabId, retryCount) { 231 | console.log('❌ [Background] 等待AI回复超时'); 232 | try { 233 | chrome.tabs.remove(tabId); 234 | chatTabId = null; 235 | retryIfNeeded(retryCount); 236 | } catch (error) { 237 | console.log('关闭超时标签页失败:', error); 238 | } 239 | } 240 | 241 | // 验证返回的消息是否有效 242 | function isValidResponse(data) { 243 | return data && 244 | data.text && 245 | data.text.trim().length > 0 && 246 | !data.text.includes('请求太快') && 247 | !data.text.includes('服务器错误') && 248 | !data.text.includes('正在思考') && 249 | !data.text.includes('正在输入') && 250 | data.text.trim().length > 10 && 251 | // 确保消息看起来是完整的 252 | (data.text.endsWith('.') || 253 | data.text.endsWith('。') || 254 | data.text.endsWith('!') || 255 | data.text.endsWith('!') || 256 | data.text.endsWith('?') || 257 | data.text.endsWith('?')); 258 | } 259 | 260 | // 修改background中的消息监听器 261 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 262 | if (request.action === 'newMessage') { 263 | console.log('📥 [Background] 收到新消息:', request.data); 264 | 265 | if (request.data && request.data.text) { 266 | // 获取所有标签页 267 | chrome.tabs.query({}, (tabs) => { 268 | // 遍历所有标签页,找到我们的扩展页面 269 | tabs.forEach(tab => { 270 | if (tab.id !== chatTabId) { // 不发送给豆包标签页 271 | try { 272 | chrome.tabs.sendMessage(tab.id, { 273 | action: 'displayResponse', 274 | data: { 275 | time: new Date().toLocaleString(), 276 | text: request.data.text, 277 | status: 'replied', 278 | html: request.data.rawHTML 279 | } 280 | }); 281 | } catch (error) { 282 | console.error(`发送消息到标签页 ${tab.id} 失败:`, error); 283 | } 284 | } 285 | }); 286 | }); 287 | 288 | // 延迟关闭窗口 289 | setTimeout(() => { 290 | if (chatTabId) { 291 | chrome.tabs.get(chatTabId, (tab) => { 292 | if (tab && tab.windowId) { 293 | chrome.windows.remove(tab.windowId, () => { 294 | console.log('✅ [Background] 豆包对话窗口已关闭'); 295 | chatTabId = null; 296 | }); 297 | } 298 | }); 299 | } 300 | }, 500); 301 | } 302 | } 303 | return true; 304 | }); 305 | 306 | // 添加一个新的监听器来确认扩展页面已准备好 307 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 308 | if (request.action === 'pageReady') { 309 | console.log('📝 [Background] 页面已准备好接收消息'); 310 | sendResponse({ status: 'acknowledged' }); 311 | } 312 | return true; 313 | }); 314 | 315 | // 处理失败响应 316 | function handleFailedResponse(tabId, retryCount) { 317 | console.log('❌ [Background] AI回复无效或出错'); 318 | try { 319 | chrome.tabs.remove(tabId); 320 | chatTabId = null; 321 | retryIfNeeded(retryCount); 322 | } catch (error) { 323 | console.log('关闭失败标签页失败:', error); 324 | } 325 | } 326 | 327 | // 重试逻辑 328 | function retryIfNeeded(retryCount) { 329 | if (retryCount < CONFIG.MAX_RETRIES) { 330 | console.log(`🔄 [Background] 尝试重试 (${retryCount + 1}/${CONFIG.MAX_RETRIES})`); 331 | setTimeout(() => { 332 | createNewChatTab(retryCount + 1); 333 | }, CONFIG.RETRY_DELAY); 334 | } else { 335 | console.log('❌ [Background] 达到最大重试次数,操作失败'); 336 | // 通知用户操作失败 337 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 338 | if (tabs[0]) { 339 | chrome.tabs.sendMessage(tabs[0].id, { 340 | action: 'displayError', 341 | data: { 342 | message: '发送图片失败,请稍后重试' 343 | } 344 | }); 345 | } 346 | }); 347 | } 348 | } 349 | 350 | // 修改executePasteScript函数 351 | function executePasteScript(tabId) { 352 | console.log('🚀 [Background] 开始执行粘贴脚本在标签页:', tabId); 353 | chrome.scripting.executeScript({ 354 | target: { tabId: tabId }, 355 | func: () => { 356 | console.log('🎯 [Page] 脚本开始执行'); 357 | 358 | let messageComplete = false; 359 | let lastMessageContent = ''; 360 | let noChangeCount = 0; 361 | const STABLE_THRESHOLD = 3; 362 | 363 | // 获取所有AI回复消息的函数 364 | const getAllAIMessages = () => { 365 | const messages = []; 366 | const xpath = '//*[@id="root"]/div[1]/div/div[2]/div[1]/div[1]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div/div/div/div/div[1]/div'; 367 | const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 368 | 369 | for (let i = 0; i < result.snapshotLength; i++) { 370 | const node = result.snapshotItem(i); 371 | const text = node.textContent; 372 | if (!text.includes('正在思考') && !text.includes('正在输入')) { 373 | messages.push(text); 374 | } 375 | } 376 | 377 | return messages; 378 | }; 379 | 380 | // 监听新消息 381 | const observer = new MutationObserver((mutations) => { 382 | const xpath = '//*[@id="root"]/div[1]/div/div[2]/div[1]/div[1]/div/div/div[2]/div/div[1]/div/div/div[2]/div[last()]/div/div/div/div/div/div[1]/div'; 383 | const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); 384 | const messageContainer = result.singleNodeValue; 385 | 386 | if (messageContainer && !messageComplete) { 387 | const currentContent = messageContainer.textContent; 388 | 389 | if (!currentContent.includes('正在思考') && !currentContent.includes('正在输入')) { 390 | if (currentContent === lastMessageContent) { 391 | noChangeCount++; 392 | if (noChangeCount >= STABLE_THRESHOLD && !messageComplete) { 393 | if (isValidMessage(currentContent)) { 394 | messageComplete = true; 395 | 396 | // 获取所有AI回复消息 397 | const allMessages = getAllAIMessages(); 398 | const fullMessage = allMessages.join('\n'); 399 | console.log('📝 [Page] 收集到的整消息:', fullMessage); 400 | 401 | const messageData = { 402 | rawHTML: messageContainer.outerHTML, 403 | text: fullMessage 404 | }; 405 | 406 | // 发送消息并停止观察 407 | chrome.runtime.sendMessage({ 408 | action: 'newMessage', 409 | data: messageData 410 | }); 411 | 412 | observer.disconnect(); 413 | } 414 | } 415 | } else { 416 | noChangeCount = 0; 417 | lastMessageContent = currentContent; 418 | } 419 | } 420 | } 421 | }); 422 | 423 | // 检查消息是否有效 424 | function isValidMessage(content) { 425 | if (!content || content.length < 10) return false; 426 | if (content.includes('正在思考') || content.includes('正在输入')) return false; 427 | if (content.includes('请求太快') || content.includes('服务器错误')) return false; 428 | 429 | // 检查是否是完整句子 430 | const endsWithPunctuation = /[.。!!??]$/.test(content.trim()); 431 | return endsWithPunctuation; 432 | } 433 | 434 | // 设置观察器配置 435 | const observerConfig = { 436 | childList: true, 437 | subtree: true, 438 | characterData: true, 439 | characterDataOldValue: true 440 | }; 441 | 442 | // 观察整个对话区域 443 | const chatContainer = document.querySelector('#root'); 444 | if (chatContainer) { 445 | console.log('🔍 [Page] 开始监听消息区域'); 446 | observer.observe(chatContainer, observerConfig); 447 | } else { 448 | console.error('❌ [Page] 未找到消息容器'); 449 | } 450 | 451 | // 修改粘贴和发送的逻辑 452 | const waitForElement = (selector, maxAttempts = 10) => { 453 | return new Promise((resolve, reject) => { 454 | let attempts = 0; 455 | const check = () => { 456 | attempts++; 457 | const element = document.querySelector(selector); 458 | if (element) { 459 | resolve(element); 460 | } else if (attempts >= maxAttempts) { 461 | reject(new Error(`Element ${selector} not found after ${maxAttempts} attempts`)); 462 | } else { 463 | setTimeout(check, 1000); 464 | } 465 | }; 466 | check(); 467 | }); 468 | }; 469 | 470 | // 执行粘贴和发送 471 | const executeActions = async () => { 472 | try { 473 | // 等待输入框出现 474 | const textArea = await waitForElement('textarea[data-testid="chat_input_input"]'); 475 | console.log('✅ [Page] 找到输入框'); 476 | 477 | // 聚焦并粘贴 478 | textArea.focus(); 479 | document.execCommand('paste'); 480 | console.log('✅ [Page] 粘贴完成'); 481 | 482 | // 等待发送按钮变为可用状态 483 | await new Promise(resolve => setTimeout(resolve, 2000)); 484 | 485 | // 检查发送按钮状态 486 | const checkAndClickSend = async (attempts = 0) => { 487 | const sendButton = document.querySelector('button[data-testid="chat_input_send_button"]'); 488 | if (sendButton && !sendButton.disabled) { 489 | sendButton.click(); 490 | console.log('✅ [Page] 发送按钮已点击'); 491 | return true; 492 | } else if (attempts < 5) { 493 | console.log(`⏳ [Page] 等待发送按钮可用 (${attempts + 1}/5)`); 494 | await new Promise(resolve => setTimeout(resolve, 1000)); 495 | return checkAndClickSend(attempts + 1); 496 | } else { 497 | throw new Error('发送按钮未能变为可用状态'); 498 | } 499 | }; 500 | 501 | await checkAndClickSend(); 502 | } catch (error) { 503 | console.error('❌ [Page] 执行操作失败:', error); 504 | chrome.runtime.sendMessage({ 505 | action: 'executionError', 506 | error: error.message 507 | }); 508 | } 509 | }; 510 | 511 | executeActions(); 512 | } 513 | }, (results) => { 514 | if (chrome.runtime.lastError) { 515 | console.error('❌ [Background] 执行脚本失败:', chrome.runtime.lastError); 516 | } else { 517 | console.log('✅ [Background] 脚本执行成功:', results); 518 | } 519 | }); 520 | } 521 | 522 | // 监听标签页关闭事件 523 | chrome.tabs.onRemoved.addListener((tabId) => { 524 | if (tabId === chatTabId) { 525 | chatTabId = null; // 重置标签页ID 526 | } 527 | }); 528 | 529 | // 添加新的错误处理监听器 530 | chrome.runtime.onMessage.addListener((request, sender) => { 531 | if (request.action === 'executionError') { 532 | console.error('❌ [Background] 执行错误:', request.error); 533 | handleFailedResponse(sender.tab.id, 0); // 触发重试机制 534 | } 535 | }); 536 | 537 | // 添加窗口关闭处理 538 | chrome.windows.onRemoved.addListener((windowId) => { 539 | chrome.tabs.query({ windowId }, (tabs) => { 540 | if (tabs && tabs[0] && tabs[0].id === chatTabId) { 541 | chatTabId = null; 542 | console.log('🔄 [Background] 豆包对话窗口已关闭,重置状态'); 543 | } 544 | }); 545 | }); 546 | 547 | // 添加新的消息监听器 548 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 549 | if (request.action === 'checkClipboard') { 550 | // 检查剪贴板内容 551 | navigator.clipboard.read() 552 | .then(clipboardItems => { 553 | const hasImage = clipboardItems.some(item => 554 | item.types.includes('image/png') || 555 | item.types.includes('image/jpeg') 556 | ); 557 | sendResponse({ hasImage }); 558 | }) 559 | .catch(error => { 560 | console.error('检查剪贴板失败:', error); 561 | sendResponse({ hasImage: false, error: error.message }); 562 | }); 563 | return true; 564 | } 565 | }); -------------------------------------------------------------------------------- /src/scripts/content.js: -------------------------------------------------------------------------------- 1 | class ElementSelector { 2 | constructor() { 3 | this.isSelecting = false; 4 | this.currentElement = null; 5 | this.highlightClass = 'screenshot-highlight'; 6 | this.mode = 'capture'; 7 | this.isPasting = false; 8 | this.init(); 9 | } 10 | 11 | init() { 12 | // 监听来自 popup 的消息 13 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 14 | if (request.action === 'startCapture') { 15 | this.mode = 'capture'; 16 | this.startSelection(); 17 | sendResponse({ status: 'success' }); 18 | return true; 19 | } else if (request.action === 'startPaste') { 20 | if (this.isPasting) { 21 | console.log('已有粘贴操作在进行中,忽略本次请求'); 22 | sendResponse({ status: 'ignored' }); 23 | return true; 24 | } 25 | this.directPaste(); 26 | sendResponse({ status: 'success' }); 27 | return true; 28 | } else if (request.action === 'getPromptText') { 29 | const promptInput = document.querySelector('#promptInput'); 30 | sendResponse(promptInput ? promptInput.value : null); 31 | } 32 | }); 33 | 34 | // 鼠标移动事件 35 | document.addEventListener('mousemove', (e) => { 36 | if (!this.isSelecting) return; 37 | 38 | console.log('mousemove event', this.isSelecting); 39 | const element = document.elementFromPoint(e.clientX, e.clientY); 40 | if (element === this.currentElement) return; 41 | 42 | this.updateHighlight(element); 43 | }); 44 | 45 | // 点击事件 46 | document.addEventListener('click', async (e) => { 47 | console.log('click event', this.isSelecting); 48 | if (!this.isSelecting) return; 49 | 50 | e.preventDefault(); 51 | e.stopPropagation(); 52 | 53 | const element = this.currentElement; 54 | this.stopSelection(); 55 | 56 | if (this.mode === 'capture') { 57 | await this.captureElement(element); 58 | } else if (this.mode === 'paste') { 59 | await this.pasteToElement(element); 60 | } 61 | }); 62 | 63 | // ESC 键取消选择 64 | document.addEventListener('keydown', (e) => { 65 | if (e.key === 'Escape' && this.isSelecting) { 66 | this.stopSelection(); 67 | } 68 | }); 69 | 70 | // 添加键盘事监听 71 | document.addEventListener('keydown', function(event) { 72 | // 检查是否按下ESC键 73 | if (event.key === 'Escape') { 74 | // 查找并关闭弹出界面 75 | const popup = document.querySelector('.ai-assistant-popup'); 76 | if (popup) { 77 | popup.remove(); 78 | } 79 | 80 | // 同时移除任何遮罩层 81 | const overlay = document.querySelector('.ai-assistant-overlay'); 82 | if (overlay) { 83 | overlay.remove(); 84 | } 85 | } 86 | }); 87 | } 88 | 89 | startSelection() { 90 | this.isSelecting = true; 91 | console.log('startSelection: 设置选择状态', this.isSelecting); 92 | document.body.classList.add('selecting'); 93 | document.body.style.cursor = 'crosshair'; 94 | // 阻止面板的点击事件冒泡 95 | const panel = document.querySelector('.floating-panel'); 96 | if (panel) { 97 | panel.style.pointerEvents = 'none'; 98 | } 99 | } 100 | 101 | stopSelection() { 102 | console.log('stopSelection: 清理选择状态'); 103 | this.isSelecting = false; 104 | document.body.classList.remove('selecting'); 105 | document.body.style.cursor = 'default'; 106 | if (this.currentElement) { 107 | this.currentElement.classList.remove(this.highlightClass); 108 | this.currentElement = null; 109 | } 110 | // 恢复面板的点击事件 111 | const panel = document.querySelector('.floating-panel'); 112 | if (panel) { 113 | panel.style.pointerEvents = 'auto'; 114 | } 115 | } 116 | 117 | updateHighlight(element) { 118 | console.log('updateHighlight: 更新高亮元素'); 119 | if (this.currentElement) { 120 | this.currentElement.classList.remove(this.highlightClass); 121 | } 122 | this.currentElement = element; 123 | if (element) { 124 | element.classList.add(this.highlightClass); 125 | } 126 | } 127 | 128 | async captureElement(element) { 129 | if (!element) return; 130 | try { 131 | console.log('开始截图流程...'); 132 | 133 | // 更新状态提示 134 | this.updateStatus('⚡ 正在截取精彩瞬间...'); 135 | 136 | // 清空剪贴板 137 | try { 138 | await navigator.clipboard.writeText(''); 139 | } catch (error) { 140 | console.log('清空剪贴板失败:', error); 141 | } 142 | 143 | const rect = element.getBoundingClientRect(); 144 | chrome.runtime.sendMessage({ 145 | action: 'captureVisibleTab', 146 | data: { 147 | rect: { 148 | x: rect.x, 149 | y: rect.y, 150 | width: rect.width, 151 | height: rect.height 152 | }, 153 | devicePixelRatio: window.devicePixelRatio 154 | } 155 | }, async response => { 156 | if (response.status === 'success') { 157 | this.updateStatus('🎯 截图完成,正在把图片发给 AI...'); 158 | await new Promise(resolve => setTimeout(resolve, 1000)); 159 | 160 | try { 161 | const clipboardItems = await navigator.clipboard.read(); 162 | const hasImage = clipboardItems.some(item => 163 | item.types.includes('image/png') || 164 | item.types.includes('image/jpeg') 165 | ); 166 | 167 | if (hasImage) { 168 | this.updateStatus('⏳ AI 正在绞尽脑汁思考中...'); 169 | this.directPaste(); 170 | } else { 171 | this.updateStatus('❌ 截图失败,请重试'); 172 | } 173 | } catch (error) { 174 | this.updateStatus('❌ 处理图片时出错'); 175 | console.error('检查剪贴失败:', error); 176 | } 177 | } 178 | }); 179 | } catch (error) { 180 | this.updateStatus('❌ 截图过程出错'); 181 | console.error('截图过程失败:', error); 182 | } 183 | } 184 | 185 | async pasteToElement(element) { 186 | console.log('开始粘贴流程...'); 187 | console.log('选中元素详细信息:', { 188 | tagName: element.tagName, 189 | id: element.id, 190 | className: element.className, 191 | attributes: Array.from(element.attributes).map(attr => ({ 192 | name: attr.name, 193 | value: attr.value 194 | })), 195 | innerHTML: element.innerHTML.substring(0, 200) + '...' // 只显示前200个字符 196 | }); 197 | 198 | try { 199 | // 1. 找到并处理输入框 200 | const textArea = element.matches('textarea') ? 201 | element : 202 | element.querySelector('textarea[data-testid="chat_input_input"]'); 203 | 204 | if (!textArea) { 205 | console.error('未找到输入框, 当前元素结构:', { 206 | element: element.outerHTML.substring(0, 200), 207 | children: Array.from(element.children).map(child => ({ 208 | tagName: child.tagName, 209 | className: child.className 210 | })) 211 | }); 212 | throw new Error('未到输入框'); 213 | } 214 | console.log('找到输入框:', { 215 | tagName: textArea.tagName, 216 | id: textArea.id, 217 | className: textArea.className, 218 | attributes: Array.from(textArea.attributes).map(attr => ({ 219 | name: attr.name, 220 | value: attr.value 221 | })) 222 | }); 223 | 224 | // 2. 粘贴图片 225 | textArea.focus(); 226 | await this.tryAllPasteMethods(textArea); 227 | console.log('图片粘完成'); 228 | 229 | // 3. 等待图片上 230 | await new Promise(resolve => setTimeout(resolve, 1500)); 231 | console.log('等待图片上传完成'); 232 | 233 | // 4. 处理发送按钮 234 | const sendButton = document.querySelector('button[data-testid="chat_input_send_button"]'); 235 | if (!sendButton) { 236 | console.error('未找到发送按钮, 当前页面所有按钮:', 237 | Array.from(document.querySelectorAll('button')).map(btn => ({ 238 | text: btn.textContent, 239 | className: btn.className, 240 | attributes: Array.from(btn.attributes).map(attr => ({ 241 | name: attr.name, 242 | value: attr.value 243 | })) 244 | })) 245 | ); 246 | throw new Error('未找到送按钮'); 247 | } 248 | 249 | console.log('找到发送按钮:', { 250 | text: sendButton.textContent, 251 | className: sendButton.className, 252 | disabled: sendButton.disabled, 253 | ariaDisabled: sendButton.getAttribute('aria-disabled'), 254 | attributes: Array.from(sendButton.attributes).map(attr => ({ 255 | name: attr.name, 256 | value: attr.value 257 | })), 258 | computedStyle: { 259 | display: window.getComputedStyle(sendButton).display, 260 | visibility: window.getComputedStyle(sendButton).visibility, 261 | opacity: window.getComputedStyle(sendButton).opacity, 262 | pointerEvents: window.getComputedStyle(sendButton).pointerEvents 263 | } 264 | }); 265 | 266 | // 5. 等待按钮可用并发送 267 | await this.waitForButtonActive(sendButton); 268 | await this.triggerSendButton(sendButton); 269 | 270 | console.log('整个流程执行完成'); 271 | this.showTooltip(element, '图片已成功发送'); 272 | 273 | } catch (error) { 274 | console.error('操作失败:', error); 275 | this.showTooltip(element, '作失败: ' + error.message); 276 | } 277 | } 278 | 279 | async triggerSendButton(button) { 280 | console.log('触发发送按钮前状态:', { 281 | disabled: button.disabled, 282 | ariaDisabled: button.getAttribute('aria-disabled'), 283 | className: button.className, 284 | style: button.style, 285 | computedStyle: { 286 | display: window.getComputedStyle(button).display, 287 | visibility: window.getComputedStyle(button).visibility, 288 | opacity: window.getComputedStyle(button).opacity, 289 | pointerEvents: window.getComputedStyle(button).pointerEvents 290 | } 291 | }); 292 | 293 | // 移除所有禁用状态 294 | button.removeAttribute('disabled'); 295 | button.setAttribute('aria-disabled', 'false'); 296 | button.style.pointerEvents = 'auto'; 297 | button.classList.remove('semi-button-disabled'); 298 | button.classList.remove('semi-button-primary-disabled'); 299 | 300 | console.log('移除禁用状态后:', { 301 | disabled: button.disabled, 302 | ariaDisabled: button.getAttribute('aria-disabled'), 303 | className: button.className, 304 | style: button.style 305 | }); 306 | 307 | // 等待DOM更新 308 | await new Promise(resolve => setTimeout(resolve, 100)); 309 | 310 | // 触发点击事件 311 | const events = ['mousedown', 'mouseup', 'click']; 312 | for (const eventType of events) { 313 | const event = new MouseEvent(eventType, { 314 | view: window, 315 | bubbles: true, 316 | cancelable: true, 317 | buttons: 1 318 | }); 319 | console.log(`触发${eventType}事件`); 320 | const result = button.dispatchEvent(event); 321 | console.log(`${eventType}事件结果:`, result); 322 | } 323 | 324 | console.log('发送按钮已触发'); 325 | } 326 | 327 | async waitForButtonActive(button, timeout = 10000) { 328 | return new Promise((resolve, reject) => { 329 | const checkInterval = 100; 330 | let elapsed = 0; 331 | 332 | const check = () => { 333 | // 检查多个条件 334 | const isActive = !button.disabled && 335 | button.getAttribute('aria-disabled') === 'false' && 336 | !button.classList.contains('semi-button-disabled'); 337 | 338 | console.log('检查按钮状态:', { 339 | disabled: button.disabled, 340 | ariaDisabled: button.getAttribute('aria-disabled'), 341 | hasDisabledClass: button.classList.contains('semi-button-disabled') 342 | }); 343 | 344 | if (isActive) { 345 | console.log('发送按钮已激活'); 346 | resolve(); 347 | } else if (elapsed >= timeout) { 348 | reject(new Error('等待按钮激活超时')); 349 | } else { 350 | elapsed += checkInterval; 351 | setTimeout(check, checkInterval); 352 | } 353 | }; 354 | 355 | check(); 356 | }); 357 | } 358 | 359 | async tryAllPasteMethods(element) { 360 | try { 361 | console.log('开始尝试粘贴方法...'); 362 | 363 | // 1. 获取剪贴板内容 364 | const clipboardItems = await navigator.clipboard.read(); 365 | console.log('获取剪贴板内容:', { 366 | itemCount: clipboardItems.length, 367 | types: clipboardItems.map(item => item.types) 368 | }); 369 | 370 | const imageItem = clipboardItems.find(item => item.types.includes('image/png')); 371 | 372 | if (!imageItem) { 373 | throw new Error('剪贴板中没有图片'); 374 | } 375 | 376 | const blob = await imageItem.getType('image/png'); 377 | console.log('获取到图片blob:', { 378 | size: blob.size, 379 | type: blob.type 380 | }); 381 | 382 | // 方1: DataTransfer with File 383 | try { 384 | console.log('尝试法1: DataTransfer with File'); 385 | const dataTransfer = new DataTransfer(); 386 | const file = new File([blob], 'image.png', { type: 'image/png' }); 387 | dataTransfer.items.add(file); 388 | 389 | const pasteEvent = new ClipboardEvent('paste', { 390 | bubbles: true, 391 | cancelable: true, 392 | clipboardData: dataTransfer 393 | }); 394 | 395 | const result = element.dispatchEvent(pasteEvent); 396 | console.log('方法1结果:', result); 397 | 398 | if (await this.checkPasteSuccess(element)) { 399 | console.log('方法1成功'); 400 | this.showTooltip(element, '方法1成功:DataTransfer with File'); 401 | return; 402 | } 403 | } catch (error) { 404 | console.log('方法1失败:', error); 405 | } 406 | 407 | throw new Error('没有粘贴方法都失败'); 408 | 409 | } catch (error) { 410 | console.error('粘贴过程失败:', error); 411 | throw error; 412 | } 413 | } 414 | 415 | checkPasteSuccess(element) { 416 | // 等待一小段时间确保事件已处理 417 | return new Promise(resolve => { 418 | setTimeout(() => { 419 | // 这添加检查粘是否成功的逻辑 420 | // 可以检查元素的值、内容或其他状态 421 | const hasContent = element.value !== '' || 422 | element.innerHTML !== '' || 423 | element.querySelector('img') !== null; 424 | resolve(hasContent); 425 | }, 100); 426 | }); 427 | } 428 | 429 | showTooltip(element, message) { 430 | const tooltip = document.createElement('div'); 431 | tooltip.textContent = message; 432 | tooltip.style.cssText = ` 433 | position: fixed; 434 | background: rgba(0, 0, 0, 0.8); 435 | color: white; 436 | padding: 8px 12px; 437 | border-radius: 4px; 438 | font-size: 14px; 439 | z-index: 999999; 440 | pointer-events: none; 441 | transition: opacity 0.3s; 442 | max-width: 300px; 443 | word-wrap: break-word; 444 | `; 445 | 446 | const rect = element.getBoundingClientRect(); 447 | tooltip.style.left = `${rect.left}px`; 448 | tooltip.style.top = `${rect.bottom + 5}px`; 449 | 450 | document.body.appendChild(tooltip); 451 | 452 | setTimeout(() => { 453 | tooltip.style.opacity = '0'; 454 | setTimeout(() => { 455 | document.body.removeChild(tooltip); 456 | }, 300); 457 | }, 5000); 458 | } 459 | 460 | // 增加直接粘贴方法 461 | async directPaste() { 462 | try { 463 | console.log('🚀 开始粘贴操作...'); 464 | 465 | // 发送消息给 background script 来创建新标签页 466 | chrome.runtime.sendMessage({ 467 | action: 'openChatTab' 468 | }); 469 | 470 | } catch (error) { 471 | console.error('❌ 操作失败:', error); 472 | } 473 | } 474 | 475 | // 添加更新状态的辅助方法 476 | updateStatus(message) { 477 | const statusTitle = document.querySelector('.response-container h3'); 478 | if (statusTitle) { 479 | statusTitle.textContent = message; 480 | 481 | // 根据消息类型设置不同的状态样式 482 | if (message.includes('❌')) { 483 | statusTitle.className = 'status-error'; 484 | } else if (message.includes('正在把图片发给 AI')) { 485 | statusTitle.className = 'status-sending'; 486 | } else if (message.includes('正在')) { 487 | statusTitle.className = 'status-thinking'; 488 | } else if (message.includes('截图完成')) { 489 | statusTitle.className = 'status-captured'; 490 | } else if (message.includes('完成')) { 491 | statusTitle.className = 'status-replied'; 492 | } else { 493 | statusTitle.className = 'status-waiting'; 494 | } 495 | } 496 | } 497 | } 498 | 499 | function createFloatingPanel() { 500 | const panel = document.createElement('div'); 501 | panel.className = 'floating-panel'; 502 | panel.innerHTML = ` 503 |
504 | AI助手 吾爱ID:hehehero 505 |
506 | 507 | × 508 |
509 |
510 |
511 |
512 | 515 | 518 | 521 |
522 |
523 |

🚀 AI 已打满鸡血,等待您的操作...

524 |
525 |
526 |
527 | `; 528 | 529 | // 添加清空按钮事件处 530 | const clearBtn = panel.querySelector('#clearBtn'); 531 | clearBtn.addEventListener('click', () => { 532 | // 清空返回内容 533 | const responseContent = panel.querySelector('#responseContent'); 534 | const statusTitle = panel.querySelector('.response-container h3'); 535 | 536 | if (responseContent) { 537 | responseContent.innerHTML = ''; 538 | } 539 | 540 | if (statusTitle) { 541 | statusTitle.textContent = '🚀 AI 已打满鸡血,等待您操作...'; 542 | statusTitle.className = 'status-waiting'; 543 | } 544 | 545 | console.log('已清空返回内容'); 546 | }); 547 | 548 | // 隐藏粘贴按钮 549 | const pasteBtn = panel.querySelector('#pasteBtn'); 550 | pasteBtn.style.display = 'none'; // 完全隐藏粘贴按钮 551 | 552 | // 验证按钮是否创建成功 553 | const captureBtn = panel.querySelector('#captureBtn'); 554 | console.log('按钮创建状态:', { 555 | captureBtn: !!captureBtn, 556 | pasteBtn: !!pasteBtn 557 | }); 558 | 559 | // 添加拖动功能 560 | let isDragging = false; 561 | let initialX; 562 | let initialY; 563 | 564 | const header = panel.querySelector('.panel-header'); 565 | 566 | header.addEventListener('mousedown', e => { 567 | isDragging = true; 568 | panel.classList.add('dragging'); 569 | 570 | const rect = panel.getBoundingClientRect(); 571 | initialX = e.clientX - rect.left; 572 | initialY = e.clientY - rect.top; 573 | }); 574 | 575 | document.addEventListener('mousemove', e => { 576 | if (!isDragging) return; 577 | 578 | e.preventDefault(); 579 | requestAnimationFrame(() => { 580 | const x = e.clientX - initialX; 581 | const y = e.clientY - initialY; 582 | 583 | panel.style.left = `${x}px`; 584 | panel.style.top = `${y}px`; 585 | panel.style.right = 'auto'; 586 | }); 587 | }); 588 | 589 | document.addEventListener('mouseup', () => { 590 | if (!isDragging) return; 591 | isDragging = false; 592 | panel.classList.remove('dragging'); 593 | }); 594 | 595 | // 修改按钮事件处理 596 | panel.querySelector('#captureBtn').addEventListener('click', (e) => { 597 | e.stopPropagation(); 598 | console.log('点击截图按钮'); 599 | if (selector.isSelecting) { 600 | selector.stopSelection(); 601 | } 602 | selector.mode = 'capture'; 603 | selector.startSelection(); 604 | }); 605 | 606 | panel.querySelector('#pasteBtn').addEventListener('click', async () => { 607 | console.log('点击粘贴按钮'); 608 | // 更新标题 609 | const titleElement = document.querySelector('.response-container h3'); 610 | if (titleElement) { 611 | titleElement.textContent = '⏳ AI 正在绞尽脑汁思考中....'; 612 | } 613 | 614 | // 清空返回内容 615 | const responseContent = document.querySelector('#responseContent'); 616 | if (responseContent) { 617 | responseContent.innerHTML = ` 618 |
619 | 时间: 620 | ${new Date().toLocaleString()} 621 |
622 |
623 |

返回内容:

624 |
等待 AI 回复...
625 |
626 | `; 627 | } 628 | 629 | await selector.directPaste(); 630 | }); 631 | 632 | // 添加最小化/恢复功能 633 | const minimizeBtn = panel.querySelector('.minimize-btn'); 634 | let isMinimized = false; 635 | 636 | minimizeBtn.addEventListener('click', (e) => { 637 | e.stopPropagation(); 638 | isMinimized = !isMinimized; 639 | panel.classList.toggle('minimized'); 640 | minimizeBtn.textContent = isMinimized ? '+' : '-'; 641 | 642 | if (isMinimized && selector.isSelecting) { 643 | selector.stopSelection(); 644 | } 645 | }); 646 | 647 | // 添加关闭按钮事件处理 648 | const closeBtn = panel.querySelector('.close-btn'); 649 | closeBtn.addEventListener('click', (e) => { 650 | e.stopPropagation(); 651 | panel.remove(); // 移除整个面板 652 | 653 | // 如果正在选择,停止选择状态 654 | if (selector.isSelecting) { 655 | selector.stopSelection(); 656 | } 657 | }); 658 | 659 | return panel; 660 | } 661 | 662 | // 修改显示返回内容的函数 663 | function displayResponse(messageData) { 664 | const responseContent = document.querySelector('#responseContent'); 665 | const titleElement = document.querySelector('.response-container h3'); 666 | 667 | if (titleElement) { 668 | if (messageData.status === 'thinking') { 669 | titleElement.textContent = 'AI 正在绞尽脑汁思考中....'; 670 | titleElement.className = 'status-thinking'; 671 | } else if (messageData.status === 'replied') { 672 | titleElement.textContent = 'AI 给了你一个敷衍的回复!'; 673 | titleElement.className = 'status-replied'; 674 | } 675 | } 676 | 677 | if (responseContent) { 678 | // 格式化显示内容 679 | const formattedContent = ` 680 |
681 | 时间: 682 | ${messageData.time} 683 |
684 |
685 |

返回内容:

686 |
${messageData.text || 'AI 脑子里一片空白,啥也没写'}
687 | 688 |
689 | `; 690 | 691 | responseContent.innerHTML = formattedContent; 692 | 693 | // 添加展开/收起功能 694 | const container = document.querySelector('.response-container'); 695 | const expandBtn = responseContent.querySelector('.expand-btn'); 696 | 697 | if (expandBtn) { 698 | expandBtn.addEventListener('click', () => { 699 | const isExpanded = container.classList.toggle('expanded'); 700 | expandBtn.textContent = isExpanded ? '收起内容' : '展开全部'; 701 | }); 702 | 703 | // 检查内容高度,决定是否显示展开按钮 704 | const content = responseContent.querySelector('.response-text'); 705 | if (content && content.scrollHeight <= container.clientHeight - 40) { 706 | expandBtn.style.display = 'none'; 707 | } 708 | } 709 | } 710 | } 711 | 712 | // 修改消息监听 713 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 714 | if (!selector) return; 715 | 716 | if (request.action === 'showPopup') { 717 | // 检查是否已存在面板 718 | const existingPanel = document.querySelector('.floating-panel'); 719 | if (existingPanel) { 720 | // 如果已存在面板,则聚焦它 721 | existingPanel.style.opacity = '1'; 722 | existingPanel.style.visibility = 'visible'; 723 | // 如果是最小化状态,恢复它 724 | if (existingPanel.classList.contains('minimized')) { 725 | existingPanel.classList.remove('minimized'); 726 | const minimizeBtn = existingPanel.querySelector('.minimize-btn'); 727 | if (minimizeBtn) { 728 | minimizeBtn.textContent = '-'; 729 | } 730 | } 731 | return; 732 | } 733 | 734 | // 如果不存在面板,则创建新的 735 | console.log('创建浮动面板'); 736 | const panel = createFloatingPanel(); 737 | document.body.appendChild(panel); 738 | } else if (request.action === 'startCapture') { 739 | console.log('收到开始截图消息'); 740 | selector.mode = 'capture'; 741 | selector.startSelection(); 742 | } else if (request.action === 'startPaste') { 743 | if (!selector.isPasting) { 744 | selector.directPaste(); 745 | } 746 | } else if (request.action === 'displayResponse') { 747 | console.log('收到答复内容:', request.data); 748 | displayResponse(request.data); 749 | } 750 | }); 751 | 752 | // 初始化选择器 753 | const selector = new ElementSelector(); -------------------------------------------------------------------------------- /src/scripts/popup.js: -------------------------------------------------------------------------------- 1 | // 监听消息 2 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 3 | if (request.action === 'newMessage') { 4 | // 当收到新消息时,更新显示状态 5 | const titleElement = document.querySelector('h3'); 6 | if (titleElement) { 7 | titleElement.textContent = 'AI 已生成回复信息!'; 8 | } 9 | // 处理其他消息逻辑... 10 | } 11 | return true; 12 | }); 13 | 14 | // 在页面加载完成时添加点击事件监听 15 | document.addEventListener('DOMContentLoaded', () => { 16 | // 监听粘贴按钮点击事件 17 | const pasteButton = document.querySelector('.paste-button'); 18 | if (pasteButton) { 19 | pasteButton.addEventListener('click', () => { 20 | // 点击粘贴按钮时更新显示状态 21 | const titleElement = document.querySelector('h3'); 22 | if (titleElement) { 23 | titleElement.textContent = 'AI 正在思考中....'; 24 | } 25 | }); 26 | } 27 | }); -------------------------------------------------------------------------------- /src/styles/content.css: -------------------------------------------------------------------------------- 1 | .floating-panel { 2 | position: fixed; 3 | top: 20px; 4 | right: 20px; 5 | width: 300px; 6 | min-height: 300px; 7 | max-height: 80vh; 8 | padding: 12px; 9 | background: rgba(255, 255, 255, 0.98); 10 | border-radius: 8px; 11 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); 12 | z-index: 999999; 13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 14 | display: flex; 15 | flex-direction: column; 16 | transition: box-shadow 0.3s ease; 17 | will-change: left, top; 18 | } 19 | 20 | .floating-panel:hover { 21 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); 22 | } 23 | 24 | .floating-panel.dragging { 25 | transition: none !important; 26 | cursor: move; 27 | user-select: none; 28 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); 29 | } 30 | 31 | .floating-panel.minimized { 32 | width: auto; 33 | min-width: 120px; 34 | min-height: auto; 35 | background: rgba(255, 255, 255, 0.95); 36 | } 37 | 38 | .floating-panel.minimized .panel-content { 39 | display: none; 40 | } 41 | 42 | .floating-panel.expanded { 43 | height: 800px; 44 | } 45 | 46 | .panel-header { 47 | margin: -12px -12px 0 -12px; 48 | padding: 4px 12px; 49 | background: linear-gradient(135deg, #7F7FD5, #86A8E7, #91EAE4); 50 | color: #fff; 51 | border-radius: 8px 8px 0 0; 52 | font-size: 13px; 53 | font-weight: 500; 54 | display: flex; 55 | justify-content: space-between; 56 | align-items: center; 57 | height: 32px; 58 | } 59 | 60 | .panel-header .minimize-btn, 61 | .panel-header .close-btn { 62 | cursor: pointer; 63 | color: #fff; 64 | font-size: 16px; 65 | line-height: 1; 66 | padding: 2px 6px; 67 | border-radius: 4px; 68 | opacity: 0.8; 69 | transition: all 0.2s ease; 70 | } 71 | 72 | .panel-header .minimize-btn:hover, 73 | .panel-header .close-btn:hover { 74 | opacity: 1; 75 | background-color: rgba(255, 255, 255, 0.2); 76 | } 77 | 78 | .panel-header .close-btn:hover { 79 | background-color: rgba(255, 87, 87, 0.8); 80 | } 81 | 82 | .panel-button { 83 | padding: 6px 12px; 84 | margin: 0; 85 | border: 1px solid #E6E8F0; 86 | border-radius: 6px; 87 | background-color: #fff; 88 | color: #5C6AC4; 89 | font-size: 13px; 90 | transition: all 0.2s ease; 91 | flex: 0 0 auto; 92 | white-space: nowrap; 93 | min-width: 80px; 94 | } 95 | 96 | .panel-button:hover { 97 | background-color: #F5F7FF; 98 | border-color: #7F7FD5; 99 | color: #7F7FD5; 100 | } 101 | 102 | .panel-button span { 103 | margin-right: 6px; 104 | font-size: 14px; 105 | } 106 | 107 | .screenshot-highlight { 108 | outline: 2px solid #7F7FD5 !important; 109 | outline-offset: -2px !important; 110 | background-color: rgba(127, 127, 213, 0.08) !important; 111 | } 112 | 113 | body.selecting * { 114 | cursor: crosshair !important; 115 | } 116 | 117 | .chat-container { 118 | display: none; 119 | width: 100%; 120 | height: 600px; 121 | margin-top: 16px; 122 | border-radius: 8px; 123 | overflow: hidden; 124 | border: 1px solid rgba(0, 0, 0, 0.1); 125 | } 126 | 127 | .chat-container iframe { 128 | width: 100%; 129 | height: 100%; 130 | border: none; 131 | } 132 | 133 | /* 左右分栏布局 */ 134 | .panel-content { 135 | display: flex; 136 | flex-direction: column; 137 | height: 100%; 138 | gap: 0; 139 | padding-top: 12px; 140 | } 141 | 142 | /* 左侧操作区 */ 143 | .panel-controls { 144 | display: flex; 145 | gap: 8px; 146 | justify-content: center; 147 | padding: 8px 0; 148 | border-bottom: 1px solid #E2E8F0; 149 | margin: 0; 150 | } 151 | 152 | /* 右侧内容显示区 */ 153 | .panel-display { 154 | flex: 1; 155 | display: flex; 156 | flex-direction: column; 157 | gap: 16px; 158 | max-height: calc(80vh - 80px); 159 | overflow-y: auto; 160 | } 161 | 162 | /* 修改返回内容容器样式 */ 163 | .response-container { 164 | flex: 1; 165 | background: #F7FAFC; 166 | border-radius: 6px; 167 | padding: 12px; 168 | font-size: 13px; 169 | line-height: 1.5; 170 | overflow-y: auto; 171 | min-height: 200px; 172 | max-height: 400px; /* 限制最大高度 */ 173 | margin-top: 0; 174 | position: relative; /* 用于折叠按钮定位 */ 175 | } 176 | 177 | /* 添加渐变遮罩效果 */ 178 | .response-container::after { 179 | content: ''; 180 | position: absolute; 181 | bottom: 0; 182 | left: 0; 183 | right: 0; 184 | height: 40px; 185 | background: linear-gradient(transparent, #F7FAFC); 186 | pointer-events: none; 187 | opacity: 0.8; 188 | } 189 | 190 | /* 返回内容文本样式优化 */ 191 | .response-text { 192 | position: relative; 193 | padding-bottom: 32px; /* 为展开按钮留出空间 */ 194 | } 195 | 196 | /* 展开/收起按钮样式 */ 197 | .expand-btn { 198 | position: absolute; 199 | bottom: 8px; 200 | left: 50%; 201 | transform: translateX(-50%); 202 | padding: 4px 12px; 203 | border-radius: 12px; 204 | background: #EDF2F7; 205 | color: #4A5568; 206 | font-size: 12px; 207 | cursor: pointer; 208 | border: 1px solid #E2E8F0; 209 | transition: all 0.2s ease; 210 | z-index: 1; 211 | } 212 | 213 | .expand-btn:hover { 214 | background: #E2E8F0; 215 | color: #2D3748; 216 | } 217 | 218 | /* 展开状态样式 */ 219 | .response-container.expanded { 220 | max-height: 80vh; 221 | } 222 | 223 | .response-container.expanded::after { 224 | display: none; 225 | } 226 | 227 | /* 优化滚动条样式 */ 228 | .response-container::-webkit-scrollbar { 229 | width: 6px; 230 | height: 6px; 231 | } 232 | 233 | .response-container::-webkit-scrollbar-track { 234 | background: transparent; 235 | } 236 | 237 | .response-container::-webkit-scrollbar-thumb { 238 | background: rgba(203, 213, 224, 0.8); 239 | border-radius: 3px; 240 | transition: all 0.2s ease; 241 | } 242 | 243 | .response-container:hover::-webkit-scrollbar-thumb { 244 | background: rgba(160, 174, 192, 0.8); 245 | } 246 | 247 | .response-container pre { 248 | white-space: pre-wrap; 249 | word-wrap: break-word; 250 | margin: 0; 251 | } 252 | 253 | /* 美化滚动条 */ 254 | .response-container::-webkit-scrollbar { 255 | width: 6px; 256 | height: 6px; 257 | } 258 | 259 | .response-container::-webkit-scrollbar-track { 260 | background: #f5f5f5; 261 | border-radius: 3px; 262 | } 263 | 264 | .response-container::-webkit-scrollbar-thumb { 265 | background: #ccc; 266 | border-radius: 3px; 267 | } 268 | 269 | .response-container::-webkit-scrollbar-thumb:hover { 270 | background: #999; 271 | } 272 | 273 | /* 返回内容项样式 */ 274 | .response-item { 275 | border-bottom: 1px solid #eee; 276 | padding: 12px 0; 277 | margin-bottom: 12px; 278 | } 279 | 280 | .response-time { 281 | color: #718096; 282 | font-size: 11px; 283 | margin-bottom: 6px; 284 | } 285 | 286 | .time-label { 287 | color: #A0AEC0; 288 | } 289 | 290 | .response-text h4 { 291 | margin: 6px 0; 292 | font-size: 13px; 293 | color: #4A5568; 294 | } 295 | 296 | .response-text pre { 297 | background: #FFFFFF; 298 | color: #F56565 !important; /* 使用淡红色 */ 299 | padding: 12px 16px; 300 | border-radius: 8px; 301 | border: 1px solid #E2E8F0; 302 | font-size: 13px; 303 | line-height: 1.6; 304 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 305 | white-space: pre-wrap; 306 | word-wrap: break-word; 307 | margin: 0; 308 | } 309 | 310 | /* 标题样式 */ 311 | .response-text pre b { 312 | color: #4C51BF; /* 深紫色 */ 313 | font-weight: 600; 314 | } 315 | 316 | /* 关键词高亮 */ 317 | .response-text pre strong { 318 | color: #805AD5; /* 紫色 */ 319 | font-weight: 600; 320 | } 321 | 322 | /* 重要内容 */ 323 | .response-text pre em { 324 | color: #3182CE; /* 蓝色 */ 325 | font-style: normal; 326 | font-weight: 500; 327 | } 328 | 329 | /* 特殊标记 */ 330 | .response-text pre mark { 331 | background: #FEF3C7; /* 浅黄色背景 */ 332 | color: #C05621; /* 橙色 */ 333 | padding: 2px 4px; 334 | border-radius: 3px; 335 | } 336 | 337 | /* 链接样式 */ 338 | .response-text pre a { 339 | color: #2B6CB0; /* 蓝色 */ 340 | text-decoration: none; 341 | border-bottom: 1px dashed #2B6CB0; 342 | } 343 | 344 | /* 代码片段 */ 345 | .response-text pre code { 346 | color: #6B46C1; /* 紫色 */ 347 | background: #F7FAFC; 348 | padding: 2px 4px; 349 | border-radius: 3px; 350 | font-family: 'Monaco', 'Menlo', 'Consolas', monospace; 351 | } 352 | 353 | /* 引用内容 */ 354 | .response-text pre blockquote { 355 | border-left: 3px solid #4C51BF; 356 | margin: 8px 0; 357 | padding-left: 12px; 358 | color: #4A5568; 359 | font-style: italic; 360 | } 361 | 362 | /* 列表项前的标记 */ 363 | .response-text pre ul { 364 | list-style: none; 365 | padding-left: 20px; 366 | } 367 | 368 | .response-text pre ul li::before { 369 | content: '•'; 370 | color: #4C51BF; 371 | font-weight: bold; 372 | display: inline-block; 373 | width: 1em; 374 | margin-left: -1em; 375 | } 376 | 377 | /* 数字/字母标记 */ 378 | .response-text pre ol { 379 | list-style: none; 380 | counter-reset: item; 381 | padding-left: 20px; 382 | } 383 | 384 | .response-text pre ol li::before { 385 | content: counter(item) '.'; 386 | counter-increment: item; 387 | color: #4C51BF; 388 | font-weight: bold; 389 | display: inline-block; 390 | width: 1.5em; 391 | margin-left: -1.5em; 392 | } 393 | 394 | /* 分隔线 */ 395 | .response-text pre hr { 396 | border: none; 397 | border-top: 1px solid #E2E8F0; 398 | margin: 12px 0; 399 | } 400 | 401 | /* 表格样式 */ 402 | .response-text pre table { 403 | border-collapse: collapse; 404 | width: 100%; 405 | margin: 12px 0; 406 | } 407 | 408 | .response-text pre th { 409 | background: #F7FAFC; 410 | color: #2D3748; 411 | font-weight: 600; 412 | padding: 8px; 413 | border: 1px solid #E2E8F0; 414 | } 415 | 416 | .response-text pre td { 417 | padding: 8px; 418 | border: 1px solid #E2E8F0; 419 | color: #4A5568; 420 | } 421 | 422 | /* 注释文本 */ 423 | .response-text pre .comment { 424 | color: #718096; 425 | font-style: italic; 426 | } 427 | 428 | /* 警告文本 */ 429 | .response-text pre .warning { 430 | color: #C05621; 431 | background: #FFFAF0; 432 | padding: 2px 4px; 433 | border-radius: 3px; 434 | } 435 | 436 | /* 错误文本 */ 437 | .response-text pre .error { 438 | color: #E53E3E; 439 | background: #FFF5F5; 440 | padding: 2px 4px; 441 | border-radius: 3px; 442 | } 443 | 444 | /* 成功文本 */ 445 | .response-text pre .success { 446 | color: #2F855A; 447 | background: #F0FFF4; 448 | padding: 2px 4px; 449 | border-radius: 3px; 450 | } 451 | 452 | /* 关键字 */ 453 | .response-text pre .keyword { 454 | color: #6B46C1; 455 | font-weight: 500; 456 | } 457 | 458 | /* 字符串 */ 459 | .response-text pre .string { 460 | color: #38A169; 461 | } 462 | 463 | /* 数字 */ 464 | .response-text pre .number { 465 | color: #D69E2E; 466 | } 467 | 468 | /* 函数名 */ 469 | .response-text pre .function { 470 | color: #3182CE; 471 | } 472 | 473 | /* 参数 */ 474 | .response-text pre .parameter { 475 | color: #805AD5; 476 | } 477 | 478 | /* 变量 */ 479 | .response-text pre .variable { 480 | color: #2B6CB0; 481 | } 482 | 483 | /* 优化性能相关的样式 */ 484 | .floating-panel * { 485 | backface-visibility: hidden; 486 | -webkit-backface-visibility: hidden; 487 | } 488 | 489 | /* 标题和状态文字基础样式 */ 490 | .response-container h3 { 491 | font-weight: 600; 492 | font-size: 14px; 493 | margin: 0 0 12px 0; 494 | padding-bottom: 8px; 495 | border-bottom: 1px solid #E2E8F0; 496 | transition: color 0.3s ease; 497 | } 498 | 499 | /* 初始状态 - 使用活力四射的绿色渐变 */ 500 | .response-container h3.status-waiting { 501 | color: #2D3748; 502 | background: linear-gradient(90deg, #48BB78 0%, #68D391 100%); /* 翠绿到浅绿的渐变 */ 503 | -webkit-background-clip: text; 504 | -webkit-text-fill-color: transparent; 505 | animation: shimmer 3s linear infinite; 506 | background-size: 200% auto; 507 | } 508 | 509 | /* 思考中状态 - 使用橙色渐变 */ 510 | .response-container h3.status-thinking { 511 | color: #ED8936; 512 | background: linear-gradient(90deg, #ED8936 0%, #F6AD55 100%); 513 | -webkit-background-clip: text; 514 | -webkit-text-fill-color: transparent; 515 | animation: shimmer 2s linear infinite; 516 | background-size: 200% auto; 517 | } 518 | 519 | /* 已回复状态 - 使用蓝色渐变 */ 520 | .response-container h3.status-replied { 521 | color: #4299E1; 522 | background: linear-gradient(90deg, #4299E1 0%, #63B3ED 100%); 523 | -webkit-background-clip: text; 524 | -webkit-text-fill-color: transparent; 525 | animation: shimmer 3s linear infinite; 526 | background-size: 200% auto; 527 | } 528 | 529 | /* 错误状态 - 使用红色渐变 */ 530 | .response-container h3.status-error { 531 | color: #E53E3E; 532 | background: linear-gradient(90deg, #E53E3E 0%, #FC8181 100%); 533 | -webkit-background-clip: text; 534 | -webkit-text-fill-color: transparent; 535 | animation: shimmer 2s linear infinite; 536 | background-size: 200% auto; 537 | } 538 | 539 | /* 添加微光动画效果 */ 540 | @keyframes shimmer { 541 | 0% { 542 | background-position: -200% center; 543 | } 544 | 100% { 545 | background-position: 200% center; 546 | } 547 | } 548 | 549 | /* 时间标签样式 */ 550 | .response-time { 551 | color: #718096; /* 灰色 */ 552 | font-size: 11px; 553 | margin-bottom: 8px; 554 | display: flex; 555 | align-items: center; 556 | } 557 | 558 | .time-label { 559 | color: #A0AEC0; /* 浅灰色 */ 560 | margin-right: 4px; 561 | } 562 | 563 | .time-value { 564 | color: #4A5568; /* 深灰色 */ 565 | font-weight: 500; 566 | } 567 | 568 | /* 返回内容样式 */ 569 | .response-text h4 { 570 | color: #2D3748; /* 保持标题颜色深灰 */ 571 | font-weight: 600; 572 | font-size: 13px; 573 | margin: 12px 0 8px 0; 574 | } 575 | 576 | /* 代码块样式 */ 577 | .response-text pre { 578 | background: #F7FAFC; /* 浅灰背景 */ 579 | color: #1A202C; /* 深色文字 */ 580 | padding: 12px; 581 | border-radius: 6px; 582 | border: 1px solid #E2E8F0; 583 | font-size: 12px; 584 | line-height: 1.6; 585 | font-family: 'Monaco', 'Menlo', 'Consolas', monospace; 586 | } 587 | 588 | /* 代码注释颜色 */ 589 | .response-text pre .comment { 590 | color: #718096; /* 灰色 */ 591 | } 592 | 593 | /* 错误信息样式 */ 594 | .response-text .error { 595 | color: #E53E3E; /* 红色 */ 596 | background: #FFF5F5; 597 | padding: 8px 12px; 598 | border-radius: 4px; 599 | border: 1px solid #FED7D7; 600 | margin: 8px 0; 601 | } 602 | 603 | /* 成功信息样式 */ 604 | .response-text .success { 605 | color: #2F855A; /* 绿色 */ 606 | background: #F0FFF4; 607 | padding: 8px 12px; 608 | border-radius: 4px; 609 | border: 1px solid #C6F6D5; 610 | margin: 8px 0; 611 | } 612 | 613 | /* 警告信息样式 */ 614 | .response-text .warning { 615 | color: #C05621; /* 橙色 */ 616 | background: #FFFAF0; 617 | padding: 8px 12px; 618 | border-radius: 4px; 619 | border: 1px solid #FEEBC8; 620 | margin: 8px 0; 621 | } 622 | 623 | /* 链接样式 */ 624 | .response-text a { 625 | color: #4C51BF; /* 紫色 */ 626 | text-decoration: none; 627 | border-bottom: 1px dashed #4C51BF; 628 | } 629 | 630 | .response-text a:hover { 631 | color: #5A67D8; 632 | border-bottom-style: solid; 633 | } 634 | 635 | /* 高亮文本样式 */ 636 | .response-text .highlight { 637 | background: #EBF4FF; /* 浅蓝背景 */ 638 | color: #2B6CB0; /* 蓝色文字 */ 639 | padding: 2px 4px; 640 | border-radius: 3px; 641 | } 642 | 643 | /* 引用文本样式 */ 644 | .response-text blockquote { 645 | border-left: 3px solid #7F7FD5; /* 紫色边框 */ 646 | margin: 8px 0; 647 | padding: 8px 16px; 648 | background: #F7FAFC; 649 | color: #4A5568; 650 | font-style: italic; 651 | } 652 | 653 | /* 列表样式 */ 654 | .response-text ul, .response-text ol { 655 | color: #2D3748; 656 | margin: 8px 0; 657 | padding-left: 20px; 658 | } 659 | 660 | .response-text li { 661 | margin: 4px 0; 662 | } 663 | 664 | /* 表格样式 */ 665 | .response-text table { 666 | border-collapse: collapse; 667 | width: 100%; 668 | margin: 12px 0; 669 | } 670 | 671 | .response-text th { 672 | background: #EDF2F7; 673 | color: #2D3748; 674 | font-weight: 600; 675 | padding: 8px; 676 | border: 1px solid #E2E8F0; 677 | } 678 | 679 | .response-text td { 680 | padding: 8px; 681 | border: 1px solid #E2E8F0; 682 | color: #4A5568; 683 | } 684 | 685 | /* 添加隐藏 */ 686 | .hidden { 687 | display: none !important; 688 | } 689 | 690 | /* 修改按钮布局 */ 691 | .panel-controls { 692 | display: flex; 693 | gap: 8px; 694 | justify-content: center; 695 | padding: 8px 0; 696 | border-bottom: 1px solid #E2E8F0; 697 | margin: 0; 698 | } 699 | 700 | /* 清空按钮特殊样式 */ 701 | #clearBtn { 702 | background-color: #fff; 703 | color: #718096; /* 默认使用灰色 */ 704 | border: 1px solid #E2E8F0; /* 默认使用浅灰色边框 */ 705 | transition: all 0.2s ease; /* 添加过渡效果 */ 706 | } 707 | 708 | #clearBtn:hover { 709 | background-color: #FFF5F5; 710 | border-color: #E53E3E; /* 悬停时显示红色边框 */ 711 | color: #E53E3E; /* 悬停时文字变红 */ 712 | } 713 | 714 | /* 清空按钮图标动画 */ 715 | #clearBtn span { 716 | transition: transform 0.2s ease; 717 | } 718 | 719 | #clearBtn:hover span { 720 | transform: rotate(15deg); 721 | } 722 | 723 | /* 截图完成状态 - 使用活力红色渐变 */ 724 | .response-container h3.status-captured { 725 | color: #E53E3E; 726 | background: linear-gradient(90deg, #F56565 0%, #FC8181 100%); /* 鲜红到浅红的渐变 */ 727 | -webkit-background-clip: text; 728 | -webkit-text-fill-color: transparent; 729 | animation: shimmer 2s linear infinite; 730 | background-size: 200% auto; 731 | } 732 | 733 | /* 发送状态 - 使用活力红色渐变 */ 734 | .response-container h3.status-sending { 735 | color: #E53E3E; 736 | background: linear-gradient(90deg, #E53E3E 0%, #FC8181 100%); /* 鲜红到浅红的渐变 */ 737 | -webkit-background-clip: text; 738 | -webkit-text-fill-color: transparent; 739 | animation: shimmer 2s linear infinite; 740 | background-size: 200% auto; 741 | } 742 | 743 | .panel-header .panel-controls { 744 | display: flex; 745 | gap: 4px; 746 | } 747 | 748 | .panel-header .close-btn { 749 | cursor: pointer; 750 | color: #666; 751 | font-size: 18px; 752 | line-height: 1; 753 | padding: 2px 6px; 754 | border-radius: 4px; 755 | } 756 | 757 | .panel-header .close-btn:hover { 758 | background-color: #ff4d4f; 759 | color: white; 760 | } 761 | 762 | /* 调整最小化按钮样式以匹配 */ 763 | .panel-header .minimize-btn { 764 | cursor: pointer; 765 | color: #666; 766 | font-size: 18px; 767 | line-height: 1; 768 | padding: 2px 6px; 769 | border-radius: 4px; 770 | } 771 | 772 | .panel-header .minimize-btn:hover { 773 | background-color: #1890ff; 774 | color: white; 775 | } 776 | --------------------------------------------------------------------------------