├── .DS_Store ├── .cursor └── rules │ ├── best-practices-guide.mdc │ └── project-documentation.mdc ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── PRIVACY.html ├── README.md ├── README.zh-CN.md ├── chrome-submission.zip ├── package.json ├── pnpm-lock.yaml ├── src ├── .DS_Store ├── Instructions │ ├── Instructions.html │ └── instructions.js ├── background.js ├── content │ ├── .DS_Store │ ├── components │ │ ├── DragHandle.js │ │ ├── IconManager.js │ │ ├── InputContainer.js │ │ ├── QuickActionButtons.js │ │ └── ResponseContainer.js │ ├── content.js │ ├── popup.js │ ├── services │ │ └── apiService.js │ ├── styles │ │ └── style.css │ └── utils │ │ ├── constants.js │ │ ├── markdownRenderer.js │ │ ├── popupStateManager.js │ │ ├── scrollManager.js │ │ └── themeManager.js ├── icons │ ├── .DS_Store │ ├── analyze.svg │ ├── check.svg │ ├── close.svg │ ├── closeClicked.svg │ ├── copy.svg │ ├── email.svg │ ├── explain.svg │ ├── hiddle.svg │ ├── icon128.png │ ├── icon16.png │ ├── icon24.png │ ├── icon24.svg │ ├── icon32.png │ ├── icon48.png │ ├── icon_copy.svg │ ├── keyboard.svg │ ├── logo.webp │ ├── mail.svg │ ├── redo.svg │ ├── redoClicked.svg │ ├── regenerate.svg │ ├── show.svg │ ├── summarize.svg │ └── translate.svg ├── manifest.json └── popup │ ├── EventManager.js │ ├── ModelManager.js │ ├── ProviderManager.js │ ├── ProviderUIManager.js │ ├── apiKeyManager.js │ ├── components │ └── dragHandle.js │ ├── i18n.js │ ├── popup.html │ ├── popup.js │ ├── storageManager.js │ └── uiManager.js ├── test └── selection-test.html ├── webpack.config.js └── ~ └── .cursor └── mcp.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/.DS_Store -------------------------------------------------------------------------------- /.cursor/rules/best-practices-guide.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | 你是全世界最伟大的物理世界模型 7 | 8 | 你具有强大的代码架构能力, 9 | 仔细追踪消息的完整流程 10 | 不要只关注表面的处理逻辑 11 | 找到问题的真正根源, 12 | 你每次修改代码时 必须必须必须必须(严格命令) 统筹全局先找到相关文件并阅读原始代码了解所有的实现细节后和代码上下文背景信息后才可以修改代码,代码逻辑要简单简洁直接!!! 一步到位!!!用最少的代码实现功能或解决bug,必要的话你可以重构现有代码以保持简洁但不能影响功,也就是遵循香农熵最小化,科氏复杂度(定义实现功能的最短程序长度)。 13 | 终极公式:代码质量 = 信息密度 × 可演化性 / 认知熵 14 | • 信息密度:单位代码传达的业务价值 15 | • 可演化性:应对需求变化的适应性(SOLID原则量化) 16 | • 认知熵:理解代码所需消耗的脑力资源 17 | 18 | 用高级javascript语法 语义化HTML 现代CSS3,多用各种语法糖 19 | 20 | 深入分析问题出现的根源 代码的源头 运用第一性原理 底层逻辑 从源头解决问题,找到最根本原因(比如按钮点不动,可能不是CSS问题而是事件监听没绑定),而不是添加临时解决方案 必要的话你可以重构现有代码以保持简洁但不能影响功能 21 | 22 | 从0到1全流程分析问题 或者使用反推法 23 | 24 | 尽量复用现在的代码 只关注专注于用户当前要求解决的问题 不要自己主动增加新功能 不要过度设计自作聪明加功能除非我要求你这样做 只修复当前我要求你解决的问题 根据当前存在的代码进行修改 不要添加多余的代码 除非有必要 修改的时候也需要把原有不需要的代码也彻底移除了 注意不能影响其他功能逻辑 25 | 26 | 修复bug时必须避免对bug之外的功能进行修改,只专注于修复bug。确保修复过程不会引入新的问题,只修改bug相关部分。 27 | 28 | 代码不要写死,要灵活有扩展性,预留空间 29 | 30 | 注重性能 31 | 32 | 不要破坏性更新 33 | 34 | 有必要的话更新DeepSeekAI项目文档 35 | 36 | 然后你在前面生成的没有解决问题的无效代码或者无意义或者重复代码要第一时间首先及时删除 防止代码冗余臃肿 然后才是更新功能或者解决问题 37 | 38 | UI UX界面要简洁清爽易操作,要符合人类认知,行为规律,情感刺激 ,以给用户舒适方便的感觉,遵循格式塔原理,用户操作心智成本低,非常的充满人性化,有呼吸感,就跟苹果公司的设计一样 注意暗黑模式,使用过程中给用户一种确定感的感觉 39 | 关于设计和交互这一块,要复合人类的认知和行为规律,情感需求,,比如记忆规律啥的,这样设计的界面也会更加舒适易用,同时也要跟项目本身的结合在一起,提高统一一致性 40 | 41 | 当前项目是浏览器扩展插件 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.cursor/rules/project-documentation.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # DeepSeek AI 浏览器扩展项目文档 7 | 8 | ## 项目整体架构概览 9 | 10 | DeepSeek AI 是一个基于 Chrome/Edge 扩展的 AI 助手工具,采用 Manifest V3 规范开发。项目使用 Webpack 作为构建工具,支持多语言(i18n)和主题切换。 11 | 12 | ### 技术栈 13 | - 构建工具:Webpack 5 14 | - 包管理器:pnpm 15 | - 核心依赖: 16 | - markdown-it:Markdown 渲染 17 | - highlight.js:代码高亮 18 | - mathjax:数学公式渲染 19 | - interactjs:拖拽交互 20 | - openai:AI API 调用 21 | 22 | ## 代码文件依赖关系 23 | 24 | ``` 25 | src/ 26 | ├── manifest.json # 扩展配置文件 27 | ├── background.js # 后台服务工作进程 28 | ├── content/ # 内容脚本目录 29 | ├── popup/ # 弹出窗口目录 30 | ├── icons/ # 图标资源 31 | ├── Instructions/ # 使用说明 32 | └── _locales/ # 国际化资源 33 | 34 | ``` 35 | 36 | ### 核心模块职责 37 | 38 | 1. **Background Service (background.js)** 39 | - 处理 API 请求代理 40 | - 管理扩展状态 41 | - 处理跨域请求 42 | - 管理上下文菜单 43 | 44 | 2. **Content Scripts (content/)** 45 | - 注入页面 UI 46 | - 处理用户交互 47 | - 与后台服务通信 48 | 49 | 3. **Popup (popup/)** 50 | - 提供设置界面 51 | - 管理用户配置 52 | - 展示使用说明 53 | 54 | 4. **国际化 (_locales/)** 55 | - 支持多语言界面 56 | - 管理翻译资源 57 | 58 | ## 权限与安全 59 | 60 | ### 扩展权限 61 | ```json 62 | { 63 | "permissions": [ 64 | "storage", // 存储访问 65 | "contextMenus", // 右键菜单 66 | "scripting", // 脚本注入 67 | "commands", // 快捷键 68 | "tabs" // 标签页访问 69 | ], 70 | "host_permissions": [ 71 | "" // 跨域请求 72 | ] 73 | } 74 | ``` 75 | 76 | ### 安全策略 77 | - CSP (Content Security Policy) 限制 78 | - 沙箱环境隔离 79 | - 资源访问控制 80 | 81 | ## 构建与部署 82 | 83 | ### 构建命令 84 | ```bash 85 | npm run build # 开发构建 86 | npm run build:zip # 打包为 zip 87 | npm run build:chrome # Chrome 发布包 88 | npm run build:edge # Edge 发布包 89 | npm run build:all # 构建所有版本 90 | ``` 91 | 92 | ### 构建配置 93 | - Webpack 5 配置 94 | - 资源压缩与优化 95 | - 开发热重载支持 96 | 97 | ## 待补充内容 98 | 1. 功能模块调用逻辑 99 | 2. 关键代码文件定位索引 100 | 3. API 接口文档 101 | 4. 组件系统文档 102 | 5. 事件系统文档 103 | 104 | ## Content 目录详细说明 105 | 106 | ### 目录结构 107 | ``` 108 | content/ 109 | ├── content.js # 内容脚本入口 110 | ├── popup.js # 弹出窗口逻辑 111 | ├── components/ # UI 组件 112 | │ ├── IconManager.js # AI 图标管理 113 | │ ├── QuickActionButtons.js # 快速操作按钮 114 | │ ├── InputContainer.js # 输入容器 115 | │ ├── ResponseContainer.js # 响应容器 116 | │ └── DragHandle.js # 拖拽句柄 117 | ├── services/ # 服务层 118 | │ └── apiService.js # API 服务 119 | ├── styles/ # 样式文件 120 | └── utils/ # 工具类 121 | ├── constants.js # 常量定义 122 | ├── themeManager.js # 主题管理 123 | ├── popupStateManager.js # 弹出窗口状态管理 124 | ├── markdownRenderer.js # Markdown 渲染 125 | └── scrollManager.js # 滚动管理 126 | ``` 127 | 128 | ### 核心功能模块 129 | 130 | #### 1. 内容脚本入口 (content.js) 131 | - 初始化 UI 组件 132 | - 处理文本选择事件 133 | - 管理弹出窗口生命周期 134 | - 处理用户交互事件 135 | - 与后台服务通信 136 | 137 | #### 2. UI 组件系统 138 | - **IconManager**: 管理 AI 图标显示和交互 139 | - **QuickActionButtons**: 提供快速操作按钮 140 | - **InputContainer**: 处理用户输入 141 | - **ResponseContainer**: 展示 AI 响应 142 | - **DragHandle**: 处理窗口拖拽 143 | 144 | #### 3. 服务层 145 | - **apiService**: 处理与 AI 服务的通信 146 | - 请求构建 147 | - 响应处理 148 | - 错误处理 149 | - 流式响应支持 150 | 151 | #### 4. 工具类 152 | - **constants**: 定义全局常量 153 | - **themeManager**: 管理主题切换 154 | - **popupStateManager**: 管理弹出窗口状态 155 | - **markdownRenderer**: 处理 Markdown 渲染 156 | - **scrollManager**: 管理滚动行为 157 | 158 | ### 主要功能流程 159 | 160 | 1. **文本选择流程** 161 | ```mermaid 162 | graph TD 163 | A[用户选择文本] --> B[触发选择事件] 164 | B --> C[显示 AI 图标] 165 | C --> D[用户点击图标] 166 | D --> E[创建弹出窗口] 167 | E --> F[显示快速操作按钮] 168 | ``` 169 | 170 | 2. **AI 交互流程** 171 | ```mermaid 172 | graph TD 173 | A[用户输入/选择文本] --> B[构建请求] 174 | B --> C[发送到后台服务] 175 | C --> D[接收流式响应] 176 | D --> E[渲染 Markdown] 177 | E --> F[更新 UI] 178 | ``` 179 | 180 | 3. **窗口管理流程** 181 | ```mermaid 182 | graph TD 183 | A[创建弹出窗口] --> B[初始化组件] 184 | B --> C[设置拖拽] 185 | C --> D[管理状态] 186 | D --> E[处理关闭] 187 | ``` 188 | 189 | ### 状态管理 190 | - 使用 Chrome Storage API 管理设置 191 | - 使用 popupStateManager 管理窗口状态 192 | - 使用主题管理器处理主题切换 193 | 194 | ### 事件系统 195 | - 文本选择事件 196 | - 图标点击事件 197 | - 快速操作事件 198 | - 窗口拖拽事件 199 | - 设置变更事件 200 | 201 | ## Popup 目录详细说明 202 | 203 | ### 目录结构 204 | ``` 205 | popup/ 206 | ├── popup.html # 弹出窗口HTML 207 | ├── popup.js # 弹出窗口主入口 208 | ├── i18n.js # 国际化管理 209 | ├── apiKeyManager.js # API密钥管理 210 | ├── EventManager.js # 事件管理 211 | ├── ModelManager.js # 模型管理 212 | ├── ProviderManager.js # 服务提供商管理 213 | ├── ProviderUIManager.js # 服务提供商UI管理 214 | ├── storageManager.js # 存储管理 215 | ├── uiManager.js # UI管理 216 | └── components/ # UI组件 217 | ``` 218 | 219 | ### 核心功能模块 220 | 221 | #### 1. 管理器模式架构 222 | 项目的弹出窗口采用了管理器模式架构,主要由以下管理器组成: 223 | 224 | - **PopupManager**: 中央控制器,协调各管理器 225 | - **I18nManager**: 处理国际化和多语言支持 226 | - **UiManager**: 管理UI元素和交互 227 | - **StorageManager**: 负责配置的存储和读取 228 | - **ProviderManager**: 管理AI服务提供商 229 | - **ModelManager**: 管理模型列表和选择 230 | - **ProviderUIManager**: 管理服务提供商的UI表示 231 | - **ApiKeyManager**: 管理API密钥 232 | - **EventManager**: 处理事件监听 233 | 234 | #### 2. ProviderManager 235 | ```mermaid 236 | classDiagram 237 | class ProviderManager { 238 | +defaultProviders[] 239 | +customProviders[] 240 | +hiddenProviders[] 241 | +getAllVisibleProviders() 242 | +saveCustomProvider() 243 | +hideDefaultProvider() 244 | +getProviderById() 245 | +getApiKey() 246 | +validateApiKey() 247 | +getModels() 248 | } 249 | ``` 250 | 251 | **功能职责**: 252 | - 管理默认和自定义服务提供商 253 | - 提供API密钥管理 254 | - 提供模型管理 255 | - 验证API密钥和连接 256 | 257 | #### 3. ModelManager 258 | ```mermaid 259 | classDiagram 260 | class ModelManager { 261 | +updateModelOptions() 262 | +createCustomModelDropdown() 263 | +showAddModelDialog() 264 | +showDeleteModelDialog() 265 | +handleDeleteModel() 266 | +handleSaveModel() 267 | } 268 | ``` 269 | 270 | **功能职责**: 271 | - 更新模型下拉选项 272 | - 创建自定义模型下拉菜单 273 | - 处理模型的添加和删除 274 | - 管理模型UI交互 275 | 276 | #### 4. StorageManager 277 | ```mermaid 278 | classDiagram 279 | class StorageManager { 280 | +getSettings() 281 | +saveSettings() 282 | +saveApiKey() 283 | +getApiKey() 284 | +saveModel() 285 | +getModel() 286 | +saveProvider() 287 | +getProvider() 288 | } 289 | ``` 290 | 291 | **功能职责**: 292 | - 管理用户设置的存储 293 | - 处理API密钥的安全存储 294 | - 提供统一的存储访问接口 295 | 296 | ### 主要功能流程 297 | 298 | 1. **设置初始化流程** 299 | ```mermaid 300 | graph TD 301 | A[页面加载] --> B[创建所有管理器] 302 | B --> C[I18nManager更新标签] 303 | C --> D[初始化事件监听器] 304 | D --> E[加载初始状态] 305 | E --> F[更新UI元素] 306 | ``` 307 | 308 | 2. **服务提供商切换流程** 309 | ```mermaid 310 | graph TD 311 | A[选择服务提供商] --> B[触发provider-change事件] 312 | B --> C[更新UI] 313 | C --> D[更新API密钥显示] 314 | D --> E[更新模型下拉菜单] 315 | E --> F[保存设置] 316 | ``` 317 | 318 | 3. **API密钥验证流程** 319 | ```mermaid 320 | graph TD 321 | A[输入API密钥] --> B[点击验证按钮] 322 | B --> C[调用validateApiKey] 323 | C --> D[发送测试请求] 324 | D --> E1[成功] 325 | D --> E2[失败] 326 | E1 --> F1[显示成功提示] 327 | E2 --> F2[显示错误提示] 328 | ``` 329 | 330 | ### 服务提供商系统 331 | 332 | Popup模块支持多种AI服务提供商: 333 | 334 | 1. **默认服务提供商**: 335 | - DeepSeek 336 | - OpenRouter 337 | - AiHubMix 338 | - SiliconFlow 339 | - VolcEngine 340 | - TencentCloud 341 | - IFlytekStar 342 | - BaiduCloud 343 | - Aliyun 344 | 345 | 2. **自定义服务提供商**: 346 | - 用户可以添加自定义的服务提供商 347 | - 支持自定义API端点 348 | - 支持自定义模型列表 349 | 350 | ### 事件管理系统 351 | 352 | EventManager负责管理所有UI事件: 353 | 354 | - 服务提供商切换事件 355 | - 模型选择事件 356 | - API密钥输入事件 357 | - 设置切换事件 358 | - 语言切换事件 359 | - 验证按钮点击事件 360 | - 添加/删除模型事件 361 | 362 | ## 背景服务分析 (background.js) 363 | 364 | ### 功能概述 365 | 366 | Background Service是扩展的核心后台进程,主要负责: 367 | 1. 处理跨域API请求代理 368 | 2. 管理请求的中断和取消 369 | 3. 处理上下文菜单交互 370 | 4. 提供存储访问接口 371 | 372 | ### 消息处理系统 373 | 374 | ```mermaid 375 | flowchart TD 376 | A[内容脚本] -->|发送消息| B{Background} 377 | B -->|getSettings| C[获取设置] 378 | B -->|proxyRequest| D[代理API请求] 379 | B -->|abortRequest| E[中断请求] 380 | B -->|openPopup| F[打开设置页] 381 | D -->|streamResponse| A 382 | ``` 383 | 384 | #### 1. 设置获取 (getSettings) 385 | - 从Chrome存储中读取API密钥、自定义URL等 386 | - 处理自定义服务提供商的设置 387 | - 返回完整的设置对象 388 | 389 | #### 2. 请求代理 (proxyRequest) 390 | - 接收来自内容脚本的API请求 391 | - 添加适当的API密钥和头信息 392 | - 处理流式响应 393 | - 通过消息通道返回数据 394 | 395 | #### 3. 请求中断 (abortRequest) 396 | - 使用AbortController取消正在进行的请求 397 | - 管理请求控制器映射 398 | - 清理与取消相关的资源 399 | 400 | ### 上下文菜单系统 401 | 402 | - 创建右键菜单选项 403 | - 处理菜单点击事件 404 | - 将选中的文本发送给内容脚本 405 | 406 | ### 关键实现技术 407 | 408 | 1. **流式响应处理**: 409 | - 使用ReadableStream API处理流式响应 410 | - 逐行解析流数据 411 | - 将数据块通过消息通道发送给内容脚本 412 | 413 | 2. **请求管理**: 414 | - 使用AbortController管理请求生命周期 415 | - 通过tabId映射请求控制器 416 | - 提供取消请求机制 417 | 418 | 3. **错误处理**: 419 | - 全面的请求错误捕获和处理 420 | - 详细的错误日志记录 421 | - 向内容脚本报告错误 422 | 423 | ## 功能模块调用逻辑 424 | 425 | ### 用户交互流程 426 | 427 | #### 1. 选择文本触发助手流程 428 | ```mermaid 429 | sequenceDiagram 430 | participant 用户 431 | participant 网页 432 | participant 内容脚本 433 | participant 背景服务 434 | participant AI服务 435 | 436 | 用户->>网页: 选择文本 437 | 网页->>内容脚本: 触发selectionchange事件 438 | 内容脚本->>网页: 显示AI图标 439 | 用户->>网页: 点击AI图标 440 | 内容脚本->>网页: 创建对话窗口 441 | 用户->>网页: 发送消息 442 | 内容脚本->>背景服务: 请求API调用 443 | 背景服务->>AI服务: 发送请求 444 | AI服务-->>背景服务: 流式返回响应 445 | 背景服务-->>内容脚本: 转发响应 446 | 内容脚本-->>网页: 渲染响应 447 | ``` 448 | 449 | #### 2. 设置配置流程 450 | ```mermaid 451 | sequenceDiagram 452 | participant 用户 453 | participant popup界面 454 | participant 背景服务 455 | participant 存储API 456 | 457 | 用户->>popup界面: 点击扩展图标 458 | popup界面->>存储API: 获取当前设置 459 | 存储API-->>popup界面: 返回设置 460 | popup界面->>popup界面: 更新UI 461 | 用户->>popup界面: 修改设置 462 | popup界面->>存储API: 保存设置 463 | 用户->>popup界面: 验证API密钥 464 | popup界面->>背景服务: 发送测试请求 465 | 背景服务-->>popup界面: 返回验证结果 466 | ``` 467 | 468 | #### 3. 右键菜单快捷方式 469 | ```mermaid 470 | sequenceDiagram 471 | participant 用户 472 | participant 网页 473 | participant 背景服务 474 | participant 内容脚本 475 | 476 | 用户->>网页: 选择文本 477 | 用户->>网页: 右键点击 478 | 网页->>背景服务: 触发contextMenu事件 479 | 背景服务->>内容脚本: 发送createPopup消息 480 | 内容脚本->>网页: 创建对话窗口 481 | 内容脚本->>网页: 填充选中的文本 482 | ``` 483 | 484 | ### 模块间通信流程 485 | 486 | #### 1. 内容脚本与背景服务通信 487 | ```mermaid 488 | sequenceDiagram 489 | participant 内容脚本 490 | participant 背景服务 491 | participant AI服务 492 | 493 | 内容脚本->>背景服务: chrome.runtime.sendMessage({action: "proxyRequest"}) 494 | 背景服务->>AI服务: fetch(request.url) 495 | AI服务-->>背景服务: 流式响应 496 | 背景服务->>内容脚本: chrome.tabs.sendMessage({type: "streamResponse"}) 497 | ``` 498 | 499 | #### 2. 存储访问流程 500 | ```mermaid 501 | sequenceDiagram 502 | participant 组件 503 | participant StorageManager 504 | participant ChromeStorage 505 | 506 | 组件->>StorageManager: getSettings() 507 | StorageManager->>ChromeStorage: chrome.storage.sync.get() 508 | ChromeStorage-->>StorageManager: 返回设置 509 | StorageManager-->>组件: 返回处理后的设置 510 | 511 | 组件->>StorageManager: saveSettings(settings) 512 | StorageManager->>ChromeStorage: chrome.storage.sync.set() 513 | ChromeStorage-->>StorageManager: 保存完成 514 | ``` 515 | 516 | #### 3. 主题切换流程 517 | ```mermaid 518 | sequenceDiagram 519 | participant 用户 520 | participant ThemeManager 521 | participant 文档 522 | 523 | 用户->>ThemeManager: 切换主题 524 | ThemeManager->>文档: 添加/移除暗黑模式类 525 | ThemeManager->>localStorage: 保存主题设置 526 | ``` 527 | 528 | ## 关键代码文件定位索引 529 | 530 | ### 核心功能实现 531 | 532 | | 功能 | 文件路径 | 主要函数/类 | 533 | |------|----------|------------| 534 | | 背景服务 | src/background.js | `chrome.runtime.onMessage.addListener()` | 535 | | 内容脚本入口 | src/content/content.js | `document.addEventListener('selectionchange')` | 536 | | AI图标管理 | src/content/components/IconManager.js | `createIcon()`, `createSvgIcon()` | 537 | | 弹出窗口创建 | src/content/popup.js | `createPopup()` | 538 | | API服务 | src/content/services/apiService.js | `getAIResponse()`, `processStreamData()` | 539 | | Markdown渲染 | src/content/utils/markdownRenderer.js | `render()` | 540 | | 设置菜单入口 | src/popup/popup.js | `PopupManager` | 541 | | 服务提供商管理 | src/popup/ProviderManager.js | `ProviderManager` | 542 | | 模型管理 | src/popup/ModelManager.js | `ModelManager` | 543 | | 存储管理 | src/popup/storageManager.js | `StorageManager` | 544 | 545 | ### 事件处理 546 | 547 | | 事件 | 文件路径 | 处理函数 | 548 | |------|----------|----------| 549 | | 文本选择 | src/content/content.js | `handleSelection()` | 550 | | AI图标点击 | src/content/components/IconManager.js | `handleIconClick()` | 551 | | 弹窗拖拽 | src/content/components/DragHandle.js | `initDrag()` | 552 | | 提交用户输入 | src/content/components/InputContainer.js | `handleSubmit()` | 553 | | 设置项改变 | src/popup/EventManager.js | `initProviderChangeHandler()` | 554 | | 添加模型 | src/popup/ModelManager.js | `showAddModelDialog()` | 555 | | 右键菜单点击 | src/background.js | `chrome.contextMenus.onClicked.addListener()` | 556 | 557 | ### 国际化与主题 558 | 559 | | 功能 | 文件路径 | 主要函数/类 | 560 | |------|----------|------------| 561 | | 多语言支持 | src/popup/i18n.js | `I18nManager`, `updateLabels()` | 562 | | 主题管理 | src/content/utils/themeManager.js | `initTheme()`, `toggleTheme()` | 563 | | 语言资源 | src/_locales/ | 语言JSON文件 | 564 | 565 | ### API调用与响应处理 566 | 567 | | 功能 | 文件路径 | 主要函数/类 | 568 | |------|----------|------------| 569 | | API请求构建 | src/content/services/apiService.js | `buildRequestBody()` | 570 | | API密钥管理 | src/popup/apiKeyManager.js | `ApiKeyManager` | 571 | | 响应流处理 | src/background.js | `fetch().then(response => {...})` | 572 | | 响应渲染 | src/content/components/ResponseContainer.js | `updateContent()` | 573 | 574 | ### 数据流向图 575 | 576 | ```mermaid 577 | graph TD 578 | A[用户输入] --> B[content.js] 579 | B --> C[apiService.js] 580 | C --> D[background.js] 581 | D --> E[AI服务API] 582 | E --> D 583 | D --> C 584 | C --> F[ResponseContainer.js] 585 | F --> G[markdownRenderer.js] 586 | G --> H[渲染响应] 587 | 588 | I[用户设置] --> J[popup.js] 589 | J --> K[ProviderManager.js] 590 | J --> L[ModelManager.js] 591 | J --> M[StorageManager.js] 592 | M --> N[Chrome Storage] 593 | N --> B 594 | ``` 595 | 596 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dist.zip -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontFamily": "Maple Mono NF CN, Menlo, Monaco, 'Courier New', monospace", 3 | "editor.mouseWheelZoom": true, 4 | "terminal.integrated.fontFamily": "Maple Mono NF CN" 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [DeepLifeStudio] 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /PRIVACY.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DeepSeek AI浏览器扩展隐私政策 7 | 24 | 25 | 26 |

隐私政策

27 |

最后更新日期:2024.8

28 | 29 |

1. 引言

30 |

31 | 欢迎使用DeepSeek 32 | AI浏览器扩展(以下简称"扩展")。本隐私政策旨在说明我们如何收集、使用、存储和保护您的信息。我们深知隐私的重要性,并致力于保护您的个人数据。 33 |

34 | 35 |

2. 信息收集与使用

36 |

2.1 API密钥

37 | 42 | 43 |

2.2 选定文本

44 | 48 | 49 |

2.3 AI响应

50 | 54 | 55 |

3. 数据存储

56 |

57 | 所有数据(包括API密钥)仅存储在您的本地浏览器中。我们不在任何远程服务器上存储您的个人数据。 58 |

59 | 60 |

4. 数据共享

61 |

62 | 我们不与任何第三方共享您的个人数据。您选择的文本仅用于通过DeepSeek 63 | API生成响应。 64 |

65 | 66 |

5. 安全措施

67 |

68 | 我们采取适当的技术措施来保护您的信息。然而,请注意,任何互联网传输都不能保证100%的安全性。 69 |

70 | 71 |

6. 用户权利

72 |

73 | 您可以随时通过删除扩展来移除所有本地存储的数据。您也可以在扩展设置中更改或删除您的API密钥。 74 |

75 | 76 |

7. 政策更新

77 |

我们可能会不时更新本隐私政策。任何重大变更都会通过扩展更新通知您。

78 | 79 |

8. 联系我们

80 |

如果您对本隐私政策有任何问题或疑虑,请通过以下方式联系我们:

81 |

82 | deeplifestudio@gmail.com 83 |

84 | 85 | 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 DeepSeekAI - Smart Web Assistant 2 | 3 |
4 | 5 | DeepSeekAI Logo 6 | 7 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/bjjobdlpgglckcmhgmmecijpfobmcpap)](https://chromewebstore.google.com/detail/bjjobdlpgglckcmhgmmecijpfobmcpap) 8 | [![License](https://img.shields.io/github/license/DeepLifeStudio/DeepSeekAI)](LICENSE) 9 | [![GitHub stars](https://img.shields.io/github/stars/DeepLifeStudio/DeepSeekAI)](https://github.com/DeepLifeStudio/DeepSeekAI/stargazers) 10 | 11 | [English](README.md) | [简体中文](README.zh-CN.md) 12 | 13 |
14 | 15 | ## 📖 Introduction 16 | 17 | DeepSeekAI is an unofficial browser extension powered by the [DeepSeek](https://deepseek.com) API, designed to enhance your web browsing experience with intelligent interactions. Through simple text selection, you can instantly receive AI-driven responses, making your web browsing more efficient and intelligent. 18 | 19 | > **Note**: This extension is a third-party development, not an official DeepSeek product. You need your own DeepSeek API Key to use this extension. 20 | 21 | ### 🔌 Supported API Providers: 22 | - [DeepSeek](https://deepseek.com) Official API 23 | - [ByteDance Volcengine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=OXTHJAF8) DeepSeek API ⭐ (Recommended) 24 | - [SiliconFlow](https://cloud.siliconflow.cn/i/lStn36vH) DeepSeek API 25 | - [OpenRouter](https://openrouter.ai/models) DeepSeek API 26 | - [Tencent Cloud](https://cloud.tencent.com/document/product/1772/115969) DeepSeek API 27 | - [IFlytek Star](https://training.xfyun.cn/modelService) DeepSeek API 28 | - [Baidu Cloud](https://console.bce.baidu.com/qianfan/modelcenter/model/buildIn/list) DeepSeek API 29 | - [Aliyun](https://bailian.console.aliyun.com/#/model-market) DeepSeek API 30 | 31 | ## ✨ Core Features 32 | 33 | ### 🎯 Smart Interaction 34 | - **Intelligent Text Analysis**: Select any text on web pages for instant AI analysis and responses 35 | - **Multi-turn Dialogue**: Support for continuous conversation interactions 36 | - **Quick Access**: Three ways to invoke the chat window - text selection, right-click menu, and keyboard shortcuts 37 | - **Streaming Response**: Real-time streaming display of AI responses 38 | - **Model Selection**: Choose between DeepSeek V3 and DeepSeek R1 models 39 | - **Multiple API Providers**: Support for various DeepSeek API providers to fit your needs 40 | - **Custom Providers**: Add your own custom API providers with personalized endpoints 41 | - **Custom Models**: Create and manage your own custom models for each provider 42 | 43 | ### 💎 User Experience 44 | - **Draggable Interface**: Freely drag and resize the chat window 45 | - **Window Memory**: Remember chat window size and position 46 | - **One-click Copy**: Easy copying of response content 47 | - **Regenerate**: Support for regenerating AI responses 48 | - **Keyboard Shortcuts**: Support custom shortcuts to directly pop up the chat window 49 | - **Balance Query**: Real-time API balance checking 50 | - **User Guide**: Built-in detailed usage instructions 51 | 52 | ### 🎨 Content Display 53 | - **Markdown Rendering**: Support for rich Markdown formatting, including code blocks, lists, and mathematical formulas (MathJax) 54 | - **Code Highlighting**: Syntax highlighting for multiple programming languages with copy functionality 55 | - **Multi-language Support**: UI in English/Chinese, AI responses with auto-language detection or specified language 56 | - **Dark Mode**: Automatic dark mode support based on system preferences 57 | 58 | ## 🚀 Quick Start 59 | 60 | ### Installation 61 | 62 | #### 1. Install from Store (Recommended) 63 | - [Chrome Web Store](https://chromewebstore.google.com/detail/bjjobdlpgglckcmhgmmecijpfobmcpap) 64 | - [Microsoft Edge Add-ons](https://chromewebstore.google.com/detail/deepseek-ai/bjjobdlpgglckcmhgmmecijpfobmcpap) 65 | 66 | #### 2. Manual Installation 67 | ```bash 68 | # Clone the repository 69 | git clone https://github.com/DeepLifeStudio/DeepSeekAI.git 70 | 71 | # Install dependencies 72 | pnpm install 73 | 74 | # Build the project 75 | pnpm run build 76 | ``` 77 | 78 | ### Configuration 79 | 80 | 1. Click the extension icon in your browser toolbar 81 | 2. Enter your DeepSeek API Key in the popup window 82 | 3. Configure language, model, and other preferences 83 | 4. Start using! You can: 84 | - Click the popup icon after selecting text 85 | - Right-click and select "DeepSeek AI" after text selection 86 | - Use custom shortcuts to open dialog window/close session window 87 | 88 | ## 🛠️ Tech Stack 89 | 90 | - **Frontend Framework**: JavaScript 91 | - **Build Tool**: Webpack 92 | - **API Integration**: DeepSeek API 93 | - **Styling**: CSS3 94 | - **Code Standard**: ESLint 95 | 96 | ## 🔜 Roadmap 97 | 98 | - [ ] Local history record feature 99 | - [ ] Custom prompt template support 100 | 101 | ## 🤝 Contributing 102 | 103 | All forms of contributions are welcome, whether it's new features, bug fixes, or documentation improvements. 104 | 105 | 1. Fork the repository 106 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 107 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 108 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 109 | 5. Open a Pull Request 110 | 111 | ## 📄 License 112 | 113 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 114 | 115 | ## 📮 Contact Us 116 | 117 | - Project Issues: [GitHub Issues](https://github.com/DeepLifeStudio/DeepSeekAI/issues) 118 | - Email: [1024jianghu@gmail.com](mailto:1024jianghu@gmail.com) 119 | - Twitter/X: [@DeepLifeStudio](https://x.com/DeepLifeStudio) 120 | 121 | --- 122 | 123 |
124 |

If this project helps you, please consider giving it a ⭐️

125 |
126 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # 🚀 DeepSeekAI - 智能网页助手 2 | 3 |
4 | 5 | DeepSeekAI Logo 6 | 7 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/bjjobdlpgglckcmhgmmecijpfobmcpap)](https://chromewebstore.google.com/detail/bjjobdlpgglckcmhgmmecijpfobmcpap) 8 | [![License](https://img.shields.io/github/license/DeepLifeStudio/DeepSeekAI)](LICENSE) 9 | [![GitHub stars](https://img.shields.io/github/stars/DeepLifeStudio/DeepSeekAI)](https://github.com/DeepLifeStudio/DeepSeekAI/stargazers) 10 | 11 | [English](README.md) | [简体中文](README.zh-CN.md) 12 | 13 |
14 | 15 | ## 📖 简介 16 | 17 | DeepSeekAI 是一款非官方的浏览器扩展插件,基于 [DeepSeek](https://deepseek.com) API,为用户提供智能的网页交互体验。通过简单的文本选择,即可获得 AI 驱动的实时响应,让您的网页浏览体验更加智能和高效。 18 | 19 | > **注意**:本扩展插件为第三方开发,非 DeepSeek 官方产品。使用本插件需要您自己的 DeepSeek API Key。 20 | 21 | ### 🔌 支持的 API 服务商: 22 | - [DeepSeek](https://deepseek.com) 官方 API 23 | - [字节火山引擎](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=OXTHJAF8) DeepSeek API ⭐ (推荐) 24 | - [硅基流动](https://cloud.siliconflow.cn/i/lStn36vH) DeepSeek API 25 | - [OpenRouter](https://openrouter.ai/models) DeepSeek API 26 | - [腾讯云](https://cloud.tencent.com/document/product/1772/115969) DeepSeek API 27 | - [讯飞星辰](https://training.xfyun.cn/modelService) DeepSeek API 28 | - [百度智能云](https://console.bce.baidu.com/qianfan/modelcenter/model/buildIn/list) DeepSeek API 29 | - [阿里云](https://bailian.console.aliyun.com/#/model-market) DeepSeek API 30 | 31 | ## ✨ 核心特性 32 | 33 | ### 🎯 智能交互 34 | - **智能文本分析**:支持网页任意文本选择,即时获取 AI 分析和回复 35 | - **多轮对话**:支持基础的对话功能,实现连续对话交互 36 | - **快捷操作**:支持文本选择、右键菜单和快捷键三种方式唤起对话窗口 37 | - **流式响应**:AI 回复实时流式显示,提供即时反馈 38 | - **模型选择**:支持选择 DeepSeek V3 和 DeepSeek R1 模型 39 | - **多种 API 提供商**:支持多种 DeepSeek API 服务商,满足不同需求 40 | - **自定义供应商**:支持添加自定义 API 供应商,可配置个性化接入点 41 | - **自定义模型**:支持为每个供应商创建和管理自定义模型 42 | 43 | ### 💎 用户体验 44 | - **可拖拽界面**:对话窗口支持自由拖拽和大小调整 45 | - **窗口记忆**:支持记住对话窗口的大小和位置 46 | - **一键复制**:便捷的回复内容复制功能 47 | - **重新生成**:支持重新生成 AI 回复 48 | - **快捷键支持**:支持自定义快捷键直接弹出会话窗口 49 | - **余额查询**:支持实时查询 API 余额 50 | - **使用说明**:内置详细的使用教程 51 | 52 | ### 🎨 内容展示 53 | - **Markdown 渲染**:支持丰富的 Markdown 格式,包括代码块、列表和数学公式(MathJax) 54 | - **代码高亮**:支持多种编程语言的语法高亮,并提供一键复制功能 55 | - **多语言支持**:支持中英文界面切换,AI 回复支持自动语言检测或指定语言 56 | - **暗色模式**:根据系统偏好自动切换暗色模式 57 | 58 | ## 🚀 快速开始 59 | 60 | ### 安装方式 61 | 62 | #### 1. 应用商店安装(推荐) 63 | - [Chrome Web Store](https://chromewebstore.google.com/detail/bjjobdlpgglckcmhgmmecijpfobmcpap) 64 | - [Microsoft Edge Add-ons](https://chromewebstore.google.com/detail/deepseek-ai/bjjobdlpgglckcmhgmmecijpfobmcpap) 65 | - [其他安装地址](https://www.crxsoso.com/webstore/detail/bjjobdlpgglckcmhgmmecijpfobmcpap)(支持 Chromium 内核的浏览器如 Edge/Chrome 等) 66 | - 安装方法请参考 [这里](https://www.youxiaohou.com/zh-cn/crx.html?spm=1739204947442#edge%E6%B5%8F%E8%A7%88%E5%99%A8) 67 | 68 | #### 2. 手动安装 69 | ```bash 70 | # 克隆项目 71 | git clone https://github.com/DeepLifeStudio/DeepSeekAI.git 72 | 73 | # 安装依赖 74 | pnpm install 75 | 76 | # 构建项目 77 | pnpm run build 78 | ``` 79 | 80 | ### 配置说明 81 | 82 | 1. 安装完成后,点击浏览器工具栏中的扩展图标 83 | 2. 在弹出窗口中输入您的 DeepSeek API Key 84 | 3. 根据个人偏好配置语言、模型和其他选项 85 | 4. 开始使用!您可以: 86 | - 选择网页文本后点击弹出的图标 87 | - 选择文本后右键选择 "DeepSeek AI" 88 | - 使用自定义快捷键打开对话窗口/关闭会话窗口 89 | 90 | 91 | ## 🛠️ 技术栈 92 | 93 | - **前端框架**:JavaScript 94 | - **构建工具**:Webpack 95 | - **API 集成**:DeepSeek API 96 | - **样式处理**:CSS3 97 | - **代码规范**:ESLint 98 | 99 | ## 🔜 开发路线 100 | - [ ] 添加本地历史记录功能 101 | - [ ] 支持自定义提示词模板 102 | 103 | 104 | 105 | ## 🤝 贡献指南 106 | 107 | 欢迎所有形式的贡献,无论是新功能、bug 修复还是文档改进。 108 | 109 | 1. Fork 本仓库 110 | 2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`) 111 | 3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`) 112 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 113 | 5. 开启一个 Pull Request 114 | 115 | ## 📄 许可证 116 | 117 | 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 118 | 119 | ## 📮 联系我们 120 | 121 | - 项目问题:[GitHub Issues](https://github.com/DeepLifeStudio/DeepSeekAI/issues) 122 | - 邮件联系:[1024jianghu@gmail.com](mailto:1024jianghu@gmail.com) 123 | - Twitter/X:[@DeepLifeStudio](https://x.com/DeepLifeStudio) 124 | 125 | --- 126 | 127 |
128 |

如果这个项目对您有帮助,请考虑给它一个 ⭐️

129 |
-------------------------------------------------------------------------------- /chrome-submission.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/chrome-submission.zip -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-assistant", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "build:zip": "webpack && cd dist && zip -r ../extension.zip *", 10 | "build:chrome": "npm run build:zip && mv extension.zip chrome-submission.zip", 11 | "build:edge": "npm run build:zip && mv extension.zip edge-submission.zip", 12 | "build:all": "npm run build:chrome && npm run build:edge" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "clipboard": "^2.0.11", 19 | "compression-webpack-plugin": "^11.1.0", 20 | "copy-webpack-plugin": "^12.0.2", 21 | "css-loader": "^7.1.2", 22 | "css-minimizer-webpack-plugin": "^7.0.0", 23 | "highlight.js": "^11.10.0", 24 | "image-webpack-loader": "^8.1.0", 25 | "interactjs": "^1.10.27", 26 | "markdown-it": "^14.1.0", 27 | "markdown-it-highlightjs": "^4.1.0", 28 | "markdown-it-mathjax3": "^4.3.2", 29 | "markdown-it-mermaid": "^0.2.5", 30 | "marked": "^13.0.3", 31 | "mathjax": "^3.2.2", 32 | "mermaid": "^11.5.0", 33 | "openai": "^4.82.0", 34 | "overlayscrollbars": "^2.10.0", 35 | "perfect-scrollbar": "^1.5.5", 36 | "pnpm": "^9.7.0", 37 | "style-loader": "^4.0.0", 38 | "terser-webpack-plugin": "^5.3.10", 39 | "webpack": "^5.93.0", 40 | "webpack-bundle-analyzer": "^4.10.2", 41 | "webpack-cli": "^5.1.4", 42 | "webpack-extension-reloader": "^1.1.4" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.25.2", 46 | "@babel/preset-env": "^7.25.3", 47 | "@types/mermaid": "^9.2.0", 48 | "babel-loader": "^9.1.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/.DS_Store -------------------------------------------------------------------------------- /src/Instructions/Instructions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DeepSeek AI User Guide 7 | 234 | 235 | 236 | 253 | 254 |
255 |
256 |

DeepSeek AI User Guide

257 |

Let AI Assistant Enhance Your Web Browsing Experience

258 |
259 | 260 |
261 |

Quick Start

262 | 308 |
    309 |
  1. Install DeepSeek AI Extension in Your Browser
  2. 310 |
  3. Click the Extension Icon in the Toolbar
  4. 311 |
  5. Enter Your DeepSeek API Key
  6. 312 |
  7. Select Your Preferred Answer Language
  8. 313 |
  9. Enable Shortcut Button Function
  10. 314 |
  11. Set Your Preferred Shortcut
  12. 315 |
  13. Select Text on Any Web Page to Start Chatting with AI!
  14. 316 |
317 |
318 | 319 |
320 |

Usage Method

321 |
322 |
323 |

Quick Button Usage

324 |

325 | After enabling the shortcut button in the extension settings, a convenient AI 326 | button will automatically appear when you select text on a webpage. Clicking 327 | on this button will quickly bring up the conversation window, making your 328 | operations more smooth. 329 |

330 |
331 |
332 |

Shortcut Usage

333 |

334 | You can use the custom shortcut key to bring up the conversation window 335 | regardless of whether text is selected. 336 |

337 |
338 |
339 |
340 | 341 |
342 |

Feature Features

343 |
344 |
345 |

Smart Chat

346 |

• Supports Multi-turn Dialogues, Remembers Context

347 |

• Real-time Streaming Response, Typewriter Effect

348 |

• Supports Regenerating Answers

349 |
350 |
351 |

UI Interaction

352 |

• Can Drag to Adjust Window Position and Size

353 |

• Supports Markdown Format Display

354 |

• Supports LaTeX Mathematical Formula Rendering

355 |

• One-click Copy for Code Blocks

356 |

• Supports Code Highlighting

357 |
358 |
359 |

Personalization Settings

360 |

• Customize Language Preference

361 |

• Dark Mode Auto Adaptation

362 |

• Configurable Shortcuts

363 |
364 |
365 |
366 | 367 |
368 |

Usage Tips

369 |
370 |

371 | 💡 Use Custom Shortcut Keys to Open Conversation Windows Faster 372 |

373 |
374 |
375 |

💡 Click the Copy Button on the Right Corner of Code Blocks to Quickly Copy Code Fragments

376 |
377 |
378 |

379 | 💡 If You're Not Satisfied with AI's Answer, You Can Click the Regenerate Button to Get a New Answer 380 |

381 |
382 |
383 |
384 |

Feedback and Support

385 | 400 |
401 |
402 |

Privacy Statement

403 |

404 | We value your privacy. DeepSeek AI extension will only send the text you select to 405 | API when necessary, and will not collect or store any other personal information. 406 | Your API key is only stored in the local browser. 407 |

408 |
409 |
410 | 411 | 412 | 413 | -------------------------------------------------------------------------------- /src/Instructions/instructions.js: -------------------------------------------------------------------------------- 1 | const translations = { 2 | zh: { 3 | title: "DeepSeek AI 使用说明", 4 | subtitle: "让 AI 助手为您的网页浏览体验增添智慧", 5 | quickStart: "快速开始", 6 | chromeInstall: "Chrome 商店安装", 7 | chromeDesc: "从 Chrome 网上应用店安装扩展", 8 | edgeInstall: "Edge 商店安装", 9 | edgeDesc: "从 Edge 网上应用店安装扩展", 10 | deepseekWebsite: "DeepSeek 官网", 11 | deepseekDesc: "访问 DeepSeek AI 官方网站", 12 | apiKey: "获取 API Key", 13 | apiDesc: "在 DeepSeek 平台获取您的 API 密钥", 14 | shortcuts: "快捷键设置", 15 | shortcutsDesc: "自定义扩展快捷键", 16 | github: "GitHub 仓库", 17 | githubDesc: "查看源代码和提交建议", 18 | installationSteps: [ 19 | "在浏览器中安装 DeepSeek AI 扩展", 20 | "点击工具栏中的扩展图标", 21 | "输入您的 DeepSeek API 密钥", 22 | "选择您偏好的回答语言", 23 | "开启快捷按钮功能", 24 | "设置你偏好的快捷键", 25 | "在任意网页上选择文本,开始与 AI 对话!", 26 | ], 27 | usage: "使用方法", 28 | quickButton: "快捷按钮使用", 29 | quickButtonDesc: "在扩展设置中开启快捷按钮后,选中网页文本时会自动显示一个便捷的 AI 按钮。点击该按钮即可快速呼出会话窗口,让您的操作更加流畅。", 30 | shortcutUsage: "快捷键使用", 31 | shortcutUsageDesc: 32 | "无论是否选中文本,都可以直接使用自定义快捷键呼出对话窗口。", 33 | features: "功能特点", 34 | smartChat: "智能对话", 35 | smartChat1: "• 支持多轮对话,记住上下文", 36 | smartChat2: "• 实时流式响应,打字机效果", 37 | smartChat3: "• 支持重新生成回答", 38 | uiInteraction: "界面交互", 39 | uiInteraction1: "• 可拖拽调整窗口位置和大小", 40 | uiInteraction2: "• 支持 Markdown 格式化显示", 41 | uiInteraction3: "• 支持 LaTeX 数学公式渲染", 42 | uiInteraction4: "• 代码块一键复制", 43 | uiInteraction5: "• 支持代码高亮", 44 | personalization: "个性化设置", 45 | personalization1: "• 自定义AI回复语言偏好", 46 | personalization2: "• 深色模式自动适配", 47 | personalization3: "• 可配置快捷键", 48 | tips: "使用技巧", 49 | tip1: "💡 可自定义快捷键以更快地打开对话窗口", 50 | tip2: "💡 点击代码块右上角的复制按钮,可以快速复制代码片段", 51 | tip3: "💡 如果对 AI 的回答不满意,可以点击重新生成按钮获取新的答案", 52 | feedback: "反馈与支持", 53 | feedbackDesc: 54 | "如果您喜欢 DeepSeek AI 扩展,欢迎在 Chrome 网上应用商店评分和评论,期待您的反馈!", 55 | chromeFeedback: "前往 Chrome 商店", 56 | chromeFeedbackDesc: "为 DeepSeek AI 评分和评论", 57 | privacy: "隐私说明", 58 | privacyDesc: 59 | "重视您的隐私。DeepSeek AI 扩展只会在必要时发送您选中的文本到 API,不会收集或存储任何其他个人信息。您的 API 密钥仅保存在本地浏览器中。", 60 | }, 61 | en: { 62 | title: "DeepSeek AI User Guide", 63 | subtitle: "Enhance your web browsing experience with AI assistance", 64 | quickStart: "Quick Start", 65 | chromeInstall: "Install from Chrome Web Store", 66 | chromeDesc: "Install the extension from Chrome Web Store", 67 | edgeInstall: "Install from Edge Add-ons", 68 | edgeDesc: "Install the extension from Edge Add-ons", 69 | deepseekWebsite: "DeepSeek Website", 70 | deepseekDesc: "Visit the official DeepSeek AI website", 71 | apiKey: "Get API Key", 72 | apiDesc: "Obtain your API key on the DeepSeek platform", 73 | shortcuts: "Shortcut Settings", 74 | shortcutsDesc: "Customize extension shortcuts", 75 | github: "GitHub Repository", 76 | githubDesc: "View source code and submit suggestions", 77 | installationSteps: [ 78 | "Install the DeepSeek AI extension in your browser", 79 | "Click the extension icon in the toolbar", 80 | "Enter your DeepSeek API key", 81 | "Select your preferred response language", 82 | "Enable the Quick Button feature", 83 | "Set your preferred shortcut keys", 84 | "Select text on the webpage to start a conversation with AI!", 85 | ], 86 | usage: "Usage", 87 | quickButton: "Quick Button Usage", 88 | quickButtonDesc: "When the Quick Button is enabled in extension settings, an AI button will automatically appear when you select text on a webpage. Click this button to quickly open the chat window for a smoother experience.", 89 | shortcutUsage: "Shortcut Usage", 90 | shortcutUsageDesc: 91 | "Whether or not text is selected, you can directly use custom shortcuts to bring up the dialog window.", 92 | features: "Features", 93 | smartChat: "Smart Chat", 94 | smartChat1: "• Supports multi-turn conversations with context memory", 95 | smartChat2: "• Real-time streaming responses with typewriter effect", 96 | smartChat3: "• Supports regenerating responses", 97 | uiInteraction: "UI Interaction", 98 | uiInteraction1: "• Draggable and resizable window", 99 | uiInteraction2: "• Supports Markdown formatting", 100 | uiInteraction3: "• Supports LaTeX math rendering", 101 | uiInteraction4: "• One-click code block copying", 102 | uiInteraction5: "• Supports code highlighting", 103 | personalization: "Personalization", 104 | personalization1: "• Customize AI response language preference.", 105 | personalization2: "• Automatic dark mode adaptation", 106 | personalization3: "• Configurable shortcuts", 107 | tips: "Tips", 108 | tip1: "💡 Customizable shortcuts for faster opening of the chat window.", 109 | tip2: "💡 Click the copy button on the code block to quickly copy the code snippet.", 110 | tip3: "💡 If you're not satisfied with the AI's response, click the regenerate button to get a new answer.", 111 | feedback: "Feedback & Support", 112 | feedbackDesc: 113 | "If you like the DeepSeek AI extension, please rate and review it on the Chrome Web Store. We look forward to your feedback!", 114 | chromeFeedback: "Visit Chrome Web Store", 115 | chromeFeedbackDesc: "Rate and review DeepSeek AI", 116 | privacy: "Privacy Policy", 117 | privacyDesc: 118 | "We value your privacy. The DeepSeek AI extension only sends selected text to the API when necessary and does not collect or store any other personal information. Your API key is stored locally in your browser.", 119 | }, 120 | }; 121 | 122 | let currentLang = "en"; 123 | 124 | const toggleLanguage = () => { 125 | // 移除console.log("toggleLanguage")这种简单的日志 126 | currentLang = currentLang === "zh" ? "en" : "zh"; 127 | 128 | // 确保translations对象中有对应的语言数据 129 | const langData = translations[currentLang]; 130 | if (!langData) { 131 | console.error("No translations found for", currentLang); 132 | return; 133 | } 134 | 135 | try { 136 | updateContent(); 137 | } catch (err) { 138 | console.error("Error updating content:", err); 139 | } 140 | }; 141 | 142 | const updateContent = () => { 143 | const langData = translations[currentLang]; 144 | document.getElementById("title").textContent = langData.title; 145 | document.getElementById("subtitle").textContent = langData.subtitle; 146 | document.getElementById("quick-start").textContent = langData.quickStart; 147 | document.getElementById("chrome-install").textContent = 148 | langData.chromeInstall; 149 | document.getElementById("chrome-desc").textContent = langData.chromeDesc; 150 | document.getElementById("edge-install").textContent = langData.edgeInstall; 151 | document.getElementById("edge-desc").textContent = langData.edgeDesc; 152 | document.getElementById("deepseek-website").textContent = 153 | langData.deepseekWebsite; 154 | document.getElementById("deepseek-desc").textContent = langData.deepseekDesc; 155 | document.getElementById("api-key").textContent = langData.apiKey; 156 | document.getElementById("api-desc").textContent = langData.apiDesc; 157 | document.getElementById("shortcuts").textContent = langData.shortcuts; 158 | document.getElementById("shortcuts-desc").textContent = 159 | langData.shortcutsDesc; 160 | document.getElementById("github").textContent = langData.github; 161 | document.getElementById("github-desc").textContent = langData.githubDesc; 162 | 163 | const steps = document.querySelectorAll("#installation-steps li"); 164 | steps.forEach((step, index) => { 165 | step.textContent = langData.installationSteps[index]; 166 | }); 167 | 168 | document.getElementById("usage").textContent = langData.usage; 169 | document.getElementById("quick-button").textContent = langData.quickButton; 170 | document.getElementById("quick-button-desc").textContent = langData.quickButtonDesc; 171 | document.getElementById("shortcut-usage").textContent = 172 | langData.shortcutUsage; 173 | document.getElementById("shortcut-usage-desc").textContent = 174 | langData.shortcutUsageDesc; 175 | 176 | document.getElementById("features").textContent = langData.features; 177 | document.getElementById("smart-chat").textContent = langData.smartChat; 178 | document.getElementById("smart-chat-1").textContent = langData.smartChat1; 179 | document.getElementById("smart-chat-2").textContent = langData.smartChat2; 180 | document.getElementById("smart-chat-3").textContent = langData.smartChat3; 181 | document.getElementById("ui-interaction").textContent = 182 | langData.uiInteraction; 183 | document.getElementById("ui-interaction-1").textContent = 184 | langData.uiInteraction1; 185 | document.getElementById("ui-interaction-2").textContent = 186 | langData.uiInteraction2; 187 | document.getElementById("ui-interaction-3").textContent = 188 | langData.uiInteraction3; 189 | document.getElementById("ui-interaction-4").textContent = 190 | langData.uiInteraction4; 191 | document.getElementById("ui-interaction-5").textContent = 192 | langData.uiInteraction5; 193 | document.getElementById("personalization").textContent = 194 | langData.personalization; 195 | document.getElementById("personalization-1").textContent = 196 | langData.personalization1; 197 | document.getElementById("personalization-2").textContent = 198 | langData.personalization2; 199 | document.getElementById("personalization-3").textContent = 200 | langData.personalization3; 201 | 202 | document.getElementById("tips").textContent = langData.tips; 203 | document.getElementById("tip-1").textContent = langData.tip1; 204 | document.getElementById("tip-2").textContent = langData.tip2; 205 | document.getElementById("tip-3").textContent = langData.tip3; 206 | 207 | document.getElementById("feedback").textContent = langData.feedback; 208 | document.getElementById("feedback-desc").textContent = langData.feedbackDesc; 209 | document.getElementById("chrome-feedback").textContent = 210 | langData.chromeFeedback; 211 | document.getElementById("chrome-feedback-desc").textContent = 212 | langData.chromeFeedbackDesc; 213 | 214 | document.getElementById("privacy").textContent = langData.privacy; 215 | document.getElementById("privacy-desc").textContent = langData.privacyDesc; 216 | }; 217 | 218 | document.addEventListener("DOMContentLoaded", () => { 219 | const langToggleBtn = document.getElementById("language-toggle"); 220 | if (langToggleBtn) { 221 | langToggleBtn.addEventListener("click", (e) => { 222 | e.preventDefault(); // 防止可能的默认行为 223 | toggleLanguage(); 224 | }); 225 | } else { 226 | console.error("Language toggle button not found"); 227 | } 228 | }); 229 | 230 | document 231 | .getElementById("shortcuts-link") 232 | .addEventListener("click", function (e) { 233 | e.preventDefault(); 234 | chrome.runtime.openOptionsPage(); 235 | chrome.tabs.create({ url: "chrome://extensions/shortcuts" }); 236 | }); 237 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // 在文件开头添加调试日志 2 | const requestControllers = new Map(); // 存储请求控制器 3 | 4 | // 加载自定义Provider 5 | async function loadCustomProviders() { 6 | return new Promise((resolve) => { 7 | chrome.storage.sync.get('customProviders', (data) => { 8 | resolve(data.customProviders || []); 9 | }); 10 | }); 11 | } 12 | 13 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 14 | if (request.action === "getSettings") { 15 | // 先获取自定义服务商列表,然后再获取其他设置 16 | chrome.storage.sync.get(['customProviders', 'provider'], async (initialData) => { 17 | const customProviders = initialData.customProviders || []; 18 | const provider = initialData.provider || 'deepseek'; 19 | 20 | // 构建需要获取的键名列表 21 | const keysToGet = [ 22 | "deepseekApiKey", "siliconflowApiKey", "openrouterApiKey","volcengineApiKey", 23 | "tencentcloudApiKey", "iflytekstarApiKey", "baiducloudApiKey", "aliyunApiKey", "aihubmixApiKey", 24 | "deepseekCustomApiUrl", "siliconflowCustomApiUrl", "openrouterCustomApiUrl", 25 | "volcengineCustomApiUrl", "tencentcloudCustomApiUrl", "iflytekstarCustomApiUrl", 26 | "baiducloudCustomApiUrl", "aliyunCustomApiUrl", "aihubmixCustomApiUrl", 27 | "language", "model" 28 | ]; 29 | 30 | // 为每个自定义服务商添加API key的键名 31 | customProviders.forEach(p => { 32 | if (p.id.startsWith('custom_')) { 33 | keysToGet.push(`${p.id}ApiKey`); 34 | } 35 | }); 36 | 37 | chrome.storage.sync.get(keysToGet, (data) => { 38 | let customApiKey = ''; 39 | let customApiUrl = ''; 40 | 41 | if (provider.startsWith('custom_')) { 42 | const customProvider = customProviders.find(p => p.id === provider); 43 | if (customProvider) { 44 | // 先从customProvider中获取apiKey 45 | customApiKey = customProvider.apiKey || ''; 46 | customApiUrl = customProvider.apiUrl || ''; 47 | 48 | // 如果customApiKey为空,尝试从${providerId}ApiKey中获取 49 | if (!customApiKey) { 50 | const apiKeyName = `${provider}ApiKey`; 51 | if (data[apiKeyName]) { 52 | customApiKey = data[apiKeyName]; 53 | } 54 | } 55 | } 56 | } 57 | 58 | sendResponse({ 59 | deepseekApiKey: data.deepseekApiKey || '', 60 | siliconflowApiKey: data.siliconflowApiKey || '', 61 | openrouterApiKey: data.openrouterApiKey || '', 62 | volcengineApiKey: data.volcengineApiKey || '', 63 | tencentcloudApiKey: data.tencentcloudApiKey || '', 64 | iflytekstarApiKey: data.iflytekstarApiKey || '', 65 | baiducloudApiKey: data.baiducloudApiKey || '', 66 | aliyunApiKey: data.aliyunApiKey || '', 67 | aihubmixApiKey: data.aihubmixApiKey || '', 68 | deepseekCustomApiUrl: data.deepseekCustomApiUrl || '', 69 | siliconflowCustomApiUrl: data.siliconflowCustomApiUrl || '', 70 | openrouterCustomApiUrl: data.openrouterCustomApiUrl || '', 71 | volcengineCustomApiUrl: data.volcengineCustomApiUrl || '', 72 | tencentcloudCustomApiUrl: data.tencentcloudCustomApiUrl || '', 73 | iflytekstarCustomApiUrl: data.iflytekstarCustomApiUrl || '', 74 | baiducloudCustomApiUrl: data.baiducloudCustomApiUrl || '', 75 | aliyunCustomApiUrl: data.aliyunCustomApiUrl || '', 76 | aihubmixCustomApiUrl: data.aihubmixCustomApiUrl || '', 77 | language: data.language || 'en', 78 | model: data.model || 'deepseek-chat', 79 | provider: provider, 80 | customApiKey: customApiKey, 81 | customApiUrl: customApiUrl, 82 | customProviders: customProviders 83 | }); 84 | }); 85 | }); 86 | return true; 87 | } 88 | 89 | if (request.action === "proxyRequest") { 90 | 91 | 92 | const controller = new AbortController(); 93 | const signal = controller.signal; 94 | 95 | // 存储控制器 96 | if (sender?.tab?.id) { 97 | requestControllers.set(sender.tab.id, controller); 98 | } 99 | 100 | // 修复: 确保模型参数被正确添加到请求中 101 | const processRequest = (requestBody) => { 102 | 103 | 104 | fetch(request.url, { 105 | method: request.method, 106 | headers: request.headers, 107 | body: requestBody, 108 | signal 109 | }) 110 | .then(async response => { 111 | 112 | 113 | // 如果不是流式响应,直接返回状态 114 | if (!requestBody.includes('"stream":true')) { 115 | // 尝试读取响应内容以获取更多错误信息 116 | try { 117 | const responseText = await response.text(); 118 | 119 | 120 | let responseData = null; 121 | try { 122 | responseData = JSON.parse(responseText); 123 | } catch (e) { 124 | console.log(`⚠️ 响应不是有效的JSON`); 125 | } 126 | 127 | sendResponse({ 128 | status: response.status, 129 | ok: response.ok, 130 | data: responseData, 131 | text: responseText 132 | }); 133 | } catch (error) { 134 | console.error(`❌ 读取响应内容错误:`, error); 135 | sendResponse({ 136 | status: response.status, 137 | ok: response.ok, 138 | error: error.message 139 | }); 140 | } 141 | return; 142 | } 143 | 144 | if (!response.ok) { 145 | console.error(`❌ HTTP错误! 状态码: ${response.status}`); 146 | throw new Error(`HTTP error! status: ${response.status}`); 147 | } 148 | 149 | const reader = response.body.getReader(); 150 | const decoder = new TextDecoder(); 151 | 152 | try { 153 | while (true) { 154 | const { done, value } = await reader.read(); 155 | 156 | if (done) { 157 | if (sender?.tab?.id) { 158 | chrome.tabs.sendMessage(sender.tab.id, { 159 | type: "streamResponse", 160 | response: { data: 'data: [DONE]\n\n', ok: true, done: true } 161 | }); 162 | } 163 | break; 164 | } 165 | 166 | const chunk = decoder.decode(value); 167 | const lines = chunk.split('\n'); 168 | 169 | for (const line of lines) { 170 | if (line.trim() === '') continue; 171 | if (!line.startsWith('data: ')) continue; 172 | 173 | const data = line.slice(6); 174 | if (data === '[DONE]') { 175 | if (sender?.tab?.id) { 176 | chrome.tabs.sendMessage(sender.tab.id, { 177 | type: "streamResponse", 178 | response: { data: 'data: [DONE]\n\n', ok: true, done: true } 179 | }); 180 | } 181 | break; 182 | } 183 | 184 | if (sender?.tab?.id) { 185 | chrome.tabs.sendMessage(sender.tab.id, { 186 | type: "streamResponse", 187 | response: { data: line + '\n\n', ok: true, done: false } 188 | }); 189 | } 190 | } 191 | } 192 | } catch (error) { 193 | console.error('读取响应流错误:', error); 194 | if (sender?.tab?.id) { 195 | chrome.tabs.sendMessage(sender.tab.id, { 196 | type: "streamResponse", 197 | response: { ok: false, error: error.message } 198 | }); 199 | } 200 | } finally { 201 | reader.releaseLock(); 202 | } 203 | }) 204 | .catch(error => { 205 | console.error('请求错误:', error); 206 | sendResponse({ 207 | ok: false, 208 | error: error.message 209 | }); 210 | }) 211 | .finally(() => { 212 | // 清理控制器 213 | if (sender?.tab?.id) { 214 | requestControllers.delete(sender.tab.id); 215 | } 216 | }); 217 | }; 218 | 219 | // 如果请求体中没有model参数,从storage中获取 220 | if (request.body && !request.body.includes('"model"')) { 221 | chrome.storage.sync.get(['model'], (data) => { 222 | const model = data.model || 'deepseek-chat'; 223 | const bodyObj = JSON.parse(request.body); 224 | bodyObj.model = model; 225 | request.body = JSON.stringify(bodyObj); 226 | processRequest(request.body); 227 | }); 228 | } else { 229 | // 请求体已经包含model参数,直接处理 230 | 231 | processRequest(request.body); 232 | } 233 | 234 | return true; 235 | } 236 | 237 | if (request.action === "abortRequest") { 238 | const controller = requestControllers.get(sender.tab.id); 239 | if (controller) { 240 | controller.abort(); 241 | requestControllers.delete(sender.tab.id); 242 | } 243 | sendResponse({ success: true }); 244 | return true; 245 | } 246 | 247 | if (request.action === "openPopup") { 248 | chrome.action.openPopup(); 249 | return true; 250 | } 251 | }); 252 | 253 | // Create context menu on extension installation 254 | chrome.runtime.onInstalled.addListener(() => { 255 | chrome.contextMenus.create({ 256 | id: "createPopup", 257 | title: "DeepSeek AI", 258 | contexts: ["selection"], 259 | }); 260 | }); 261 | 262 | // Handle context menu clicks 263 | chrome.contextMenus.onClicked.addListener((info, tab) => { 264 | if (info.menuItemId === "createPopup") { 265 | chrome.tabs.sendMessage(tab.id, { 266 | action: "createPopup", 267 | selectedText: info.selectionText || null, 268 | message: info.selectionText || getGreeting() 269 | }); 270 | } 271 | }); 272 | 273 | // 全局注册命令监听器 274 | chrome.commands.onCommand.addListener(async (command) => { 275 | if (command === "toggle-chat") { 276 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 277 | if (!tab || tab.url.startsWith('chrome://') || tab.url.startsWith('edge://')) { 278 | return; 279 | } 280 | 281 | try { 282 | // 使用更可靠的方式获取选中文本 283 | const [{result}] = await chrome.scripting.executeScript({ 284 | target: { tabId: tab.id }, 285 | func: () => { 286 | // 首先尝试使用window.getSelection() 287 | const selection = window.getSelection(); 288 | if (selection && selection.toString && selection.toString().trim()) { 289 | return selection.toString().trim(); 290 | } 291 | 292 | // 如果没有选中文本,尝试检查页面上可能的选中元素 293 | // 某些网站可能使用自定义选择或点击交互会清除选择 294 | // 检查是否有具有特定CSS样式的元素表明它们被选中 295 | const selectedElements = document.querySelectorAll( 296 | '.selected, .highlight, .highlighted, [aria-selected="true"], ::-moz-selection, ::selection' 297 | ); 298 | 299 | for (const el of selectedElements) { 300 | if (el.textContent && el.textContent.trim()) { 301 | return el.textContent.trim(); 302 | } 303 | } 304 | 305 | // 如果上述都失败,返回空字符串 306 | return ''; 307 | } 308 | }); 309 | 310 | 311 | // 发送toggleChat消息以实现真正的切换功能 312 | chrome.tabs.sendMessage(tab.id, { 313 | action: "toggleChat", 314 | selectedText: result, // 发送选中的文本,如果为空则使用问候语 315 | useGreeting: getGreeting() 316 | }); 317 | } catch (error) { 318 | console.error("获取选中文本出错:", error); 319 | // 即使出错,也发送消息以打开聊天窗口,只是不带选中文本 320 | chrome.tabs.sendMessage(tab.id, { 321 | action: "toggleChat", 322 | selectedText: "", 323 | useGreeting: getGreeting() 324 | }); 325 | } 326 | } else if (command === "close-chat") { 327 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 328 | if (!tab || tab.url.startsWith('chrome://') || tab.url.startsWith('edge://')) { 329 | return; 330 | } 331 | 332 | chrome.tabs.sendMessage(tab.id, { 333 | action: "closeChat" 334 | }); 335 | } 336 | }); 337 | 338 | function getGreeting() { 339 | const hour = new Date().getHours(); 340 | if (hour >= 5 && hour < 12) { 341 | return "Good morning 🥰"; 342 | } else if (hour >= 12 && hour < 18) { 343 | return "Good afternoon 🥰"; 344 | } else { 345 | return "Good evening 🥰"; 346 | } 347 | } 348 | 349 | chrome.runtime.onInstalled.addListener((details) => { 350 | if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { 351 | // 打开说明页面 352 | chrome.tabs.create({ 353 | url: chrome.runtime.getURL('Instructions/Instructions.html') 354 | }); 355 | } 356 | }); 357 | 358 | -------------------------------------------------------------------------------- /src/content/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/content/.DS_Store -------------------------------------------------------------------------------- /src/content/components/DragHandle.js: -------------------------------------------------------------------------------- 1 | export function initDraggable(dragHandle, popup) { 2 | let isDragging = false; 3 | let startX, startY; 4 | let initialX, initialY; 5 | 6 | dragHandle.addEventListener('mousedown', startDragging); 7 | document.addEventListener('mousemove', drag); 8 | document.addEventListener('mouseup', stopDragging); 9 | 10 | function startDragging(e) { 11 | isDragging = true; 12 | startX = e.clientX; 13 | startY = e.clientY; 14 | initialX = popup.offsetLeft; 15 | initialY = popup.offsetTop; 16 | popup.style.transition = 'none'; 17 | 18 | // 添加拖拽状态样式,提供更好的视觉反馈 19 | popup.classList.add('dragging'); 20 | // 改变光标样式 21 | dragHandle.style.cursor = 'grabbing'; 22 | 23 | // 添加拖拽开始的触感反馈 24 | if ('vibrate' in navigator) { 25 | navigator.vibrate(10); 26 | } 27 | } 28 | 29 | function drag(e) { 30 | if (!isDragging) return; 31 | 32 | e.preventDefault(); 33 | const dx = e.clientX - startX; 34 | const dy = e.clientY - startY; 35 | 36 | // 添加拖拽的惯性和流畅感 37 | popup.style.left = `${initialX + dx}px`; 38 | popup.style.top = `${initialY + dy}px`; 39 | 40 | // 更新窗口位置时显示距离指示器(仅当移动足够距离时) 41 | const moveDistance = Math.sqrt(dx * dx + dy * dy); 42 | if (moveDistance > 50) { 43 | // 显示拖拽距离指示器 44 | showDragDistance(popup, moveDistance.toFixed(0)); 45 | } 46 | } 47 | 48 | function stopDragging() { 49 | if (!isDragging) return; 50 | 51 | isDragging = false; 52 | popup.style.transition = 'transform 0.05s cubic-bezier(0.4, 0, 0.2, 1)'; 53 | 54 | // 移除拖拽状态样式 55 | popup.classList.remove('dragging'); 56 | // 恢复光标样式 57 | dragHandle.style.cursor = 'grab'; 58 | 59 | // 移除拖拽距离指示器 60 | const distanceIndicator = popup.querySelector('.drag-distance'); 61 | if (distanceIndicator) { 62 | distanceIndicator.remove(); 63 | } 64 | 65 | // 添加"放置"的触感反馈 66 | if ('vibrate' in navigator) { 67 | navigator.vibrate(5); 68 | } 69 | 70 | // 添加放置动画效果 71 | popup.classList.add('drag-placed'); 72 | setTimeout(() => { 73 | popup.classList.remove('drag-placed'); 74 | }, 150); 75 | } 76 | 77 | // 显示拖拽距离指示器 78 | function showDragDistance(popup, distance) { 79 | let distanceIndicator = popup.querySelector('.drag-distance'); 80 | 81 | if (!distanceIndicator) { 82 | distanceIndicator = document.createElement('div'); 83 | distanceIndicator.className = 'drag-distance'; 84 | distanceIndicator.style.position = 'absolute'; 85 | distanceIndicator.style.bottom = '-25px'; 86 | distanceIndicator.style.left = '50%'; 87 | distanceIndicator.style.transform = 'translateX(-50%)'; 88 | distanceIndicator.style.backgroundColor = 'var(--bg-secondary)'; 89 | distanceIndicator.style.color = 'var(--text-secondary)'; 90 | distanceIndicator.style.padding = '4px 8px'; 91 | distanceIndicator.style.borderRadius = '4px'; 92 | distanceIndicator.style.fontSize = '10px'; 93 | distanceIndicator.style.pointerEvents = 'none'; 94 | distanceIndicator.style.zIndex = '2000'; 95 | distanceIndicator.style.opacity = '0.9'; 96 | distanceIndicator.style.boxShadow = 'var(--shadow-sm)'; 97 | popup.appendChild(distanceIndicator); 98 | } 99 | 100 | distanceIndicator.textContent = `已移动 ${distance}px`; 101 | } 102 | 103 | return () => { 104 | dragHandle.removeEventListener('mousedown', startDragging); 105 | document.removeEventListener('mousemove', drag); 106 | document.removeEventListener('mouseup', stopDragging); 107 | }; 108 | } 109 | 110 | export function resizeMoveListener(event) { 111 | const { target, rect } = event; 112 | 113 | Object.assign(target.style, { 114 | width: `${rect.width}px`, 115 | height: `${rect.height}px` 116 | }); 117 | 118 | if (event.edges.left) { 119 | target.style.left = `${rect.left}px`; 120 | } 121 | if (event.edges.top) { 122 | target.style.top = `${rect.top}px`; 123 | } 124 | } 125 | 126 | export function createDragHandle(removeCallback) { 127 | const dragHandle = document.createElement("div"); 128 | Object.assign(dragHandle.style, { 129 | position: "absolute", 130 | top: "0", 131 | left: "0", 132 | width: "100%", 133 | height: "40px", 134 | cursor: "grab", // 改为grab光标,更符合拖拽操作的直觉 135 | display: "flex", 136 | alignItems: "center", 137 | justifyContent: "center", 138 | padding: "0 10px", 139 | boxSizing: "border-box", 140 | }); 141 | 142 | dragHandle.classList.add('drag-handle'); 143 | 144 | const titleContainer = document.createElement("div"); 145 | Object.assign(titleContainer.style, { 146 | display: "flex", 147 | alignItems: "center", 148 | userSelect: "none", 149 | WebkitUserSelect: "none", 150 | pointerEvents: "none" 151 | }); 152 | 153 | const logo = document.createElement("img"); 154 | logo.src = chrome.runtime.getURL("icons/icon24.png"); 155 | Object.assign(logo.style, { 156 | height: "24px", 157 | marginRight: "10px", 158 | userSelect: "none", 159 | WebkitUserSelect: "none", 160 | pointerEvents: "none", 161 | WebkitUserDrag: "none", 162 | WebkitAppRegion: "no-drag", 163 | draggable: false 164 | }); 165 | logo.setAttribute("draggable", "false"); 166 | 167 | const textNode = document.createElement("span"); 168 | Object.assign(textNode.style, { 169 | fontWeight: "bold", 170 | userSelect: "none", 171 | WebkitUserSelect: "none", 172 | pointerEvents: "none" 173 | }); 174 | textNode.textContent = "DeepSeek AI"; 175 | titleContainer.appendChild(logo); 176 | titleContainer.appendChild(textNode); 177 | 178 | // 添加拖拽提示 179 | const dragTooltip = document.createElement("div"); 180 | dragTooltip.className = "tool-tip"; 181 | dragTooltip.textContent = "Drag to move the window"; 182 | dragTooltip.style.top = "45px"; 183 | dragTooltip.style.left = "50%"; 184 | dragTooltip.style.transform = "translateX(-50%)"; 185 | dragHandle.appendChild(dragTooltip); 186 | 187 | const closeButton = document.createElement("button"); 188 | closeButton.className = "close-button tooltip-trigger"; 189 | Object.assign(closeButton.style, { 190 | display: "none", 191 | background: "none", 192 | border: "none", 193 | cursor: "pointer", 194 | padding: "8px", 195 | margin: "0", 196 | transition: "all 0.2s cubic-bezier(0.25, 1, 0.5, 1)", 197 | position: "absolute", 198 | right: "10px", 199 | top: "50%", 200 | transform: "translateY(-50%) scale(1)", 201 | width: "24px", 202 | height: "24px", 203 | minWidth: "24px", 204 | minHeight: "24px", 205 | maxWidth: "24px", 206 | maxHeight: "24px", 207 | lineHeight: "1", 208 | outline: "none", 209 | boxSizing: "content-box", 210 | zIndex: "10", 211 | appearance: "none", 212 | WebkitAppearance: "none", 213 | MozAppearance: "none", 214 | borderRadius: "6px", 215 | display: "flex", 216 | alignItems: "center", 217 | justifyContent: "center" 218 | }); 219 | 220 | const closeIcon = document.createElement("div"); 221 | closeIcon.className = "close-icon"; 222 | // 使用已有的关闭图标SVG 223 | closeIcon.innerHTML = ` 224 | 关闭 225 | `; 226 | 227 | // 简化图标样式 228 | Object.assign(closeIcon.style, { 229 | width: "16px", 230 | height: "16px", 231 | display: "flex", 232 | alignItems: "center", 233 | justifyContent: "center", 234 | position: "relative", 235 | color: "var(--text-secondary)", 236 | transition: "transform 0.15s ease, color 0.15s ease", 237 | padding: "0", 238 | zIndex: "10" 239 | }); 240 | 241 | closeButton.appendChild(closeIcon); 242 | 243 | // 添加关闭按钮工具提示 244 | const closeTooltip = document.createElement("div"); 245 | closeTooltip.className = "tool-tip"; 246 | closeTooltip.textContent = "关闭窗口"; 247 | closeTooltip.style.right = "16px"; 248 | closeTooltip.style.top = "45px"; 249 | dragHandle.appendChild(closeTooltip); 250 | 251 | closeButton.addEventListener("mouseenter", () => { 252 | // 悬停时变为苹果蓝色并轻微放大 253 | closeIcon.style.transform = "scale(1.1)"; 254 | const img = closeIcon.querySelector('img'); 255 | if (img) { 256 | img.style.filter = "invert(48%) sepia(57%) saturate(6300%) hue-rotate(193deg) brightness(101%) contrast(101%)"; 257 | } 258 | }); 259 | 260 | closeButton.addEventListener("mouseleave", () => { 261 | // 恢复正常状态 262 | closeIcon.style.transform = "scale(1)"; 263 | const img = closeIcon.querySelector('img'); 264 | if (img) { 265 | img.style.filter = ""; 266 | } 267 | }); 268 | 269 | closeButton.addEventListener("mousedown", (e) => { 270 | e.stopPropagation(); 271 | // 点击时只放大图标而不是整个按钮 272 | closeIcon.style.transform = "scale(1.2)"; 273 | 274 | // 强制按钮保持在原位置 275 | closeButton.style.transform = "translateY(-50%)"; 276 | closeButton.style.top = "50%"; 277 | 278 | // 触觉反馈 279 | if ('vibrate' in navigator) { 280 | navigator.vibrate(8); 281 | } 282 | }); 283 | 284 | closeButton.addEventListener("mouseup", () => { 285 | // 恢复到悬停状态,按钮保持位置不变 286 | closeIcon.style.transform = "scale(1.1)"; 287 | closeButton.style.transform = "translateY(-50%)"; 288 | }); 289 | 290 | closeButton.addEventListener("click", (e) => { 291 | e.stopPropagation(); 292 | e.preventDefault(); // 防止默认行为 293 | 294 | // 强制按钮保持在原位置 295 | closeButton.style.transform = "translateY(-50%)"; 296 | closeButton.style.transition = "none"; 297 | 298 | // 确保图标不会发生变形 299 | closeIcon.style.transition = "none"; 300 | closeIcon.style.transform = "scale(1)"; 301 | 302 | // 阻止所有可能的动画或滑动 303 | const img = closeIcon.querySelector('img'); 304 | if (img) { 305 | img.style.transition = "none"; 306 | } 307 | 308 | // 立即执行关闭回调,不添加任何额外效果 309 | if (removeCallback) { 310 | removeCallback(); 311 | } 312 | }); 313 | 314 | dragHandle.addEventListener("mouseenter", () => { 315 | closeButton.style.display = "flex"; 316 | }); 317 | 318 | dragHandle.addEventListener("mouseleave", () => { 319 | closeButton.style.display = "none"; 320 | }); 321 | 322 | dragHandle.appendChild(titleContainer); 323 | dragHandle.appendChild(closeButton); 324 | 325 | // 添加徽标,表示扩展的身份 326 | dragHandle.addEventListener("dblclick", (e) => { 327 | e.stopPropagation(); 328 | if (removeCallback) { 329 | // 双击标题栏也可以关闭 330 | removeCallback(); 331 | } 332 | }); 333 | 334 | return dragHandle; 335 | } -------------------------------------------------------------------------------- /src/content/components/IconManager.js: -------------------------------------------------------------------------------- 1 | import { getAIResponse } from '../services/apiService'; 2 | import PerfectScrollbar from "perfect-scrollbar"; 3 | import { setAllowAutoScroll } from "../utils/scrollManager"; 4 | 5 | export function createIcon(x, y) { 6 | const icon = document.createElement("img"); 7 | icon.src = chrome.runtime.getURL("icons/icon24.png"); 8 | Object.assign(icon.style, { 9 | position: "fixed", 10 | cursor: "pointer", 11 | left: `${x}px`, 12 | top: `${y}px`, 13 | width: "30px", 14 | height: "30px", 15 | zIndex: "2147483646", 16 | padding: "4px", 17 | backgroundColor: "transparent", 18 | border: "none", 19 | outline: "none", 20 | userSelect: "none", 21 | pointerEvents: "auto" 22 | }); 23 | 24 | return icon; 25 | } 26 | 27 | export function createSvgIcon(iconName, title) { 28 | const wrapper = document.createElement("div"); 29 | wrapper.className = "icon-wrapper tooltip"; 30 | wrapper.style.display = "inline-block"; 31 | 32 | const icon = document.createElement("img"); 33 | icon.style.width = "18px"; 34 | icon.style.height = "18px"; 35 | icon.src = chrome.runtime.getURL(`icons/${iconName}.svg`); 36 | icon.style.border = "none"; 37 | icon.style.cursor = "pointer"; 38 | icon.style.transition = "all 0.2s ease"; 39 | icon.style.opacity = "1"; 40 | icon.style.setProperty('--icon-color', document.body.classList.contains('theme-adaptive dark-mode') ? '#ffffff' : '#000000'); 41 | 42 | icon.addEventListener("mousedown", () => { 43 | icon.style.transform = "scale(1.2)"; 44 | }); 45 | 46 | icon.addEventListener("mouseup", () => { 47 | icon.style.transform = "scale(1)"; 48 | icon.src = chrome.runtime.getURL(`icons/${iconName}.svg`); 49 | }); 50 | 51 | const tooltip = document.createElement("span"); 52 | tooltip.className = "tooltiptext"; 53 | tooltip.textContent = title; 54 | tooltip.style.visibility = "hidden"; 55 | tooltip.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; 56 | tooltip.style.color = "white"; 57 | tooltip.style.textAlign = "center"; 58 | tooltip.style.padding = "4px 8px"; 59 | tooltip.style.borderRadius = "5px"; 60 | tooltip.style.position = "absolute"; 61 | tooltip.style.zIndex = "1"; 62 | tooltip.style.bottom = "125%"; 63 | tooltip.style.left = "50%"; 64 | tooltip.style.transform = "translateX(-50%)"; 65 | tooltip.style.whiteSpace = "nowrap"; 66 | 67 | wrapper.appendChild(icon); 68 | wrapper.appendChild(tooltip); 69 | 70 | wrapper.addEventListener("mouseenter", () => { 71 | tooltip.style.visibility = "visible"; 72 | }); 73 | 74 | wrapper.addEventListener("mouseleave", () => { 75 | tooltip.style.visibility = "hidden"; 76 | }); 77 | 78 | return wrapper; 79 | } 80 | 81 | export function addIconsToElement(element) { 82 | if (!element.textContent.trim()) { 83 | return; 84 | } 85 | 86 | const existingContainer = element.querySelector('.icon-container'); 87 | if (existingContainer) { 88 | existingContainer.remove(); 89 | } 90 | 91 | const iconContainer = document.createElement("div"); 92 | iconContainer.className = "icon-container"; 93 | iconContainer.style.display = "flex"; 94 | iconContainer.style.opacity = "0"; 95 | iconContainer.style.transition = "opacity 0.2s ease"; 96 | 97 | const copyWrapper = document.createElement("div"); 98 | copyWrapper.className = "icon-wrapper tooltip"; 99 | 100 | const copyIcon = document.createElement("img"); 101 | copyIcon.src = chrome.runtime.getURL("icons/copy.svg"); 102 | copyIcon.title = "Copy"; 103 | copyIcon.style.opacity = "1"; 104 | copyIcon.style.setProperty('--icon-color', document.body.classList.contains('theme-adaptive dark-mode') ? '#ffffff' : '#000000'); 105 | 106 | const copyTooltip = document.createElement("span"); 107 | copyTooltip.className = "tooltiptext"; 108 | copyTooltip.textContent = "Copy"; 109 | 110 | copyWrapper.appendChild(copyIcon); 111 | copyWrapper.appendChild(copyTooltip); 112 | 113 | copyWrapper.addEventListener("click", (event) => { 114 | event.stopPropagation(); 115 | const textContent = Array.from(element.childNodes) 116 | .filter(node => { 117 | // 排除图标容器和reasoning content 118 | return (!node.classList || 119 | (!node.classList.contains('icon-container') && 120 | !node.classList.contains('reasoning-content'))) 121 | }) 122 | .map(node => node.textContent) 123 | .join('') 124 | .trim() // 去除首尾空白字符 125 | .replace(/^\n+|\n+$/g, ''); // 去除开头和结尾的换行符 126 | 127 | navigator.clipboard.writeText(textContent).then(() => { 128 | copyIcon.style.transform = "scale(1.2)"; 129 | copyIcon.title = "Copied!"; 130 | copyTooltip.textContent = "Copied!"; 131 | 132 | setTimeout(() => { 133 | copyIcon.style.transform = ""; 134 | copyIcon.title = "Copy"; 135 | copyTooltip.textContent = "Copy"; 136 | }, 1000); 137 | }); 138 | }); 139 | 140 | iconContainer.appendChild(copyWrapper); 141 | 142 | if (element.classList.contains("ai-answer")) { 143 | const userQuestion = element.previousElementSibling; 144 | if (userQuestion && userQuestion.classList.contains("user-question")) { 145 | const regenerateWrapper = document.createElement("div"); 146 | regenerateWrapper.className = "icon-wrapper tooltip"; 147 | 148 | const regenerateIcon = document.createElement("img"); 149 | regenerateIcon.src = chrome.runtime.getURL("icons/regenerate.svg"); 150 | regenerateIcon.title = "Regenerate"; 151 | regenerateIcon.style.opacity = "1"; 152 | regenerateIcon.style.setProperty('--icon-color', document.body.classList.contains('theme-adaptive dark-mode') ? '#ffffff' : '#000000'); 153 | 154 | const regenerateTooltip = document.createElement("span"); 155 | regenerateTooltip.className = "tooltiptext"; 156 | regenerateTooltip.textContent = "Regenerate"; 157 | 158 | regenerateWrapper.appendChild(regenerateIcon); 159 | regenerateWrapper.appendChild(regenerateTooltip); 160 | 161 | regenerateWrapper.addEventListener("click", (event) => { 162 | event.stopPropagation(); 163 | const questionText = userQuestion.textContent; 164 | element.textContent = ""; 165 | const abortController = new AbortController(); 166 | const aiResponseContainer = window.aiResponseContainer; 167 | 168 | setAllowAutoScroll(true); 169 | 170 | requestAnimationFrame(() => { 171 | const questionTop = userQuestion.offsetTop; 172 | aiResponseContainer.scrollTop = Math.max(0, questionTop - 20); 173 | aiResponseContainer.perfectScrollbar.update(); 174 | }); 175 | 176 | getAIResponse( 177 | questionText, 178 | element, 179 | abortController.signal, 180 | aiResponseContainer.perfectScrollbar, 181 | null, 182 | aiResponseContainer, 183 | true 184 | ); 185 | }); 186 | 187 | iconContainer.appendChild(regenerateWrapper); 188 | } 189 | } 190 | 191 | element.style.position = "relative"; 192 | element.appendChild(iconContainer); 193 | 194 | element.addEventListener("mouseenter", () => { 195 | iconContainer.style.opacity = "1"; 196 | }); 197 | 198 | element.addEventListener("mouseleave", () => { 199 | iconContainer.style.opacity = "0"; 200 | }); 201 | 202 | // 修改鼠标移出事件处理 203 | element.addEventListener('mouseleave', (event) => { 204 | if (iconContainer.dataset.initialShow === 'true') { 205 | delete iconContainer.dataset.initialShow; 206 | iconContainer.style.opacity = '0'; 207 | 208 | element.addEventListener('mouseenter', () => { 209 | if (!iconContainer.dataset.initialShow) { 210 | iconContainer.style.opacity = '1'; 211 | } 212 | }); 213 | 214 | element.addEventListener('mouseleave', () => { 215 | if (!iconContainer.dataset.initialShow) { 216 | iconContainer.style.opacity = '0'; 217 | } 218 | }); 219 | } 220 | }); 221 | 222 | requestAnimationFrame(() => { 223 | updateLastAnswerIcons(); 224 | }); 225 | } 226 | 227 | export function updateLastAnswerIcons() { 228 | const aiResponseElement = document.getElementById("ai-response"); 229 | if (!aiResponseElement) return; 230 | 231 | const answers = aiResponseElement.getElementsByClassName("ai-answer"); 232 | if (!answers || answers.length === 0) return; 233 | 234 | const aiResponseContainer = document.getElementById("ai-response-container"); 235 | if (!aiResponseContainer) return; 236 | 237 | Array.from(answers).forEach(answer => { 238 | const iconContainer = answer.querySelector('.icon-container'); 239 | if (iconContainer) { 240 | const regenerateIcon = iconContainer.querySelector('img[src*="regenerate"]'); 241 | if (regenerateIcon) { 242 | regenerateIcon.parentElement.remove(); 243 | if (iconContainer.children.length === 0) { 244 | iconContainer.style.display = 'none'; 245 | } 246 | } 247 | } 248 | }); 249 | 250 | const lastAnswer = answers[answers.length - 1]; 251 | if (!lastAnswer) return; 252 | 253 | const userQuestion = lastAnswer.previousElementSibling; 254 | const iconContainer = lastAnswer.querySelector('.icon-container'); 255 | 256 | if (iconContainer && !iconContainer.querySelector('img[src*="regenerate"]') && 257 | userQuestion && userQuestion.classList.contains("user-question")) { 258 | iconContainer.style.opacity = '1'; 259 | const regenerateWrapper = document.createElement("div"); 260 | regenerateWrapper.className = "icon-wrapper tooltip"; 261 | 262 | const regenerateIcon = document.createElement("img"); 263 | regenerateIcon.src = chrome.runtime.getURL("icons/regenerate.svg"); 264 | regenerateIcon.title = "Regenerate"; 265 | regenerateIcon.style.opacity = "1"; 266 | regenerateIcon.style.setProperty('--icon-color', document.body.classList.contains('theme-adaptive dark-mode') ? '#ffffff' : '#000000'); 267 | 268 | const regenerateTooltip = document.createElement("span"); 269 | regenerateTooltip.className = "tooltiptext"; 270 | regenerateTooltip.textContent = "Regenerate"; 271 | 272 | regenerateWrapper.appendChild(regenerateIcon); 273 | regenerateWrapper.appendChild(regenerateTooltip); 274 | 275 | regenerateWrapper.addEventListener("click", (event) => { 276 | event.stopPropagation(); 277 | const questionText = userQuestion.textContent; 278 | lastAnswer.textContent = ""; 279 | const abortController = new AbortController(); 280 | let ps = aiResponseContainer.ps; 281 | if (!ps) { 282 | ps = new PerfectScrollbar(aiResponseContainer, { 283 | suppressScrollX: true, 284 | wheelPropagation: false, 285 | }); 286 | aiResponseContainer.ps = ps; 287 | } 288 | 289 | requestAnimationFrame(() => { 290 | const questionTop = userQuestion.offsetTop; 291 | aiResponseContainer.scrollTop = Math.max(0, questionTop - 20); 292 | ps.update(); 293 | }); 294 | 295 | getAIResponse( 296 | questionText, 297 | lastAnswer, 298 | abortController.signal, 299 | ps, 300 | null, 301 | aiResponseContainer, 302 | true 303 | ); 304 | }); 305 | iconContainer.appendChild(regenerateWrapper); 306 | } 307 | } 308 | 309 | window.updateLastAnswerIcons = updateLastAnswerIcons; 310 | window.addIconsToElement = addIconsToElement; -------------------------------------------------------------------------------- /src/content/components/ResponseContainer.js: -------------------------------------------------------------------------------- 1 | import PerfectScrollbar from 'perfect-scrollbar'; 2 | import { createScrollManager, getAllowAutoScroll, handleUserScroll, updateAllowAutoScroll } from '../utils/scrollManager'; 3 | import { SCROLL_CONSTANTS } from '../utils/constants'; 4 | 5 | export function styleResponseContainer(container) { 6 | container.id = "ai-response-container"; 7 | Object.assign(container.style, { 8 | flex: "1", 9 | minHeight: "0", 10 | position: "relative", 11 | marginBottom: "60px", 12 | overflowY: "auto", 13 | overflowX: "hidden", 14 | padding: "20px 10px", 15 | paddingBottom: "60px", 16 | boxSizing: "border-box", 17 | userSelect: "text", 18 | "-webkit-user-select": "text", 19 | "-moz-user-select": "text", 20 | "-ms-user-select": "text", 21 | }); 22 | 23 | const scrollManager = createScrollManager(); 24 | container.scrollStateManager = scrollManager; 25 | 26 | const handleScroll = (e) => { 27 | if (!scrollManager) return; 28 | 29 | scrollManager.setManualScrolling(true); 30 | scrollManager.saveScrollPosition(container); 31 | handleUserScroll(e); 32 | 33 | requestAnimationFrame(() => { 34 | if (container.perfectScrollbar) { 35 | container.perfectScrollbar.update(); 36 | } 37 | 38 | if (!scrollManager.isInCooldown()) { 39 | const isAtBottom = scrollManager.isNearBottom(container); 40 | updateAllowAutoScroll(container); 41 | 42 | if (isAtBottom && getAllowAutoScroll()) { 43 | const lastMessage = container.querySelector('#ai-response > div:last-child'); 44 | if (lastMessage) { 45 | const containerRect = container.getBoundingClientRect(); 46 | const messageRect = lastMessage.getBoundingClientRect(); 47 | const BUTTON_SPACE = 100; 48 | const HOVER_BUTTON_HEIGHT = 40; 49 | const extraScroll = Math.max(0, messageRect.bottom + BUTTON_SPACE + HOVER_BUTTON_HEIGHT - containerRect.bottom); 50 | if (extraScroll > 0) { 51 | container.scrollTop += extraScroll; 52 | container.perfectScrollbar?.update(); 53 | } 54 | } 55 | } 56 | } 57 | }); 58 | 59 | if (scrollManager.scrollTimeout) { 60 | clearTimeout(scrollManager.scrollTimeout); 61 | } 62 | scrollManager.scrollTimeout = setTimeout(() => { 63 | scrollManager.setManualScrolling(false); 64 | }, 150); 65 | }; 66 | 67 | container.addEventListener('scroll', handleScroll, { passive: true }); 68 | container.addEventListener('wheel', handleScroll, { passive: true }); 69 | container.addEventListener('touchstart', handleScroll, { passive: true }); 70 | container.addEventListener('touchmove', handleScroll, { passive: true }); 71 | 72 | return container; 73 | } -------------------------------------------------------------------------------- /src/content/services/apiService.js: -------------------------------------------------------------------------------- 1 | import { getAllowAutoScroll, scrollToBottom } from "../utils/scrollManager"; 2 | import { md, showCodeCopyButtons } from "../utils/markdownRenderer"; 3 | 4 | // 全局变量用于存储对话历史 5 | let messages = []; 6 | let isGenerating = false; 7 | let renderQueue = []; 8 | 9 | // 用于存储当前响应的内容 10 | let currentReasoningContent = ""; 11 | let currentContent = ""; 12 | 13 | // 使用 Performance API 优化性能监控 14 | const performance = window.performance; 15 | 16 | export function getIsGenerating() { 17 | return isGenerating; 18 | } 19 | 20 | const processText = (text, type) => { 21 | if (type === 'cleanup') { 22 | return text.trim().replace(/\s+/g, ' '); 23 | } 24 | return text; 25 | }; 26 | 27 | // 优化渲染队列处理 28 | async function processRenderQueue(responseElement, ps, aiResponseContainer) { 29 | if (!responseElement?.isConnected || !aiResponseContainer?.isConnected) { 30 | renderQueue = []; 31 | return; 32 | } 33 | 34 | const currentChunk = renderQueue[renderQueue.length - 1]; 35 | if (!currentChunk) return; 36 | 37 | try { 38 | // 获取或创建reasoning content元素 39 | if (currentChunk.reasoningContent) { 40 | let reasoningContentElement = responseElement.querySelector('.reasoning-content'); 41 | if (!reasoningContentElement) { 42 | reasoningContentElement = document.createElement('div'); 43 | reasoningContentElement.className = 'reasoning-content expanded'; 44 | reasoningContentElement.innerHTML = ` 45 |
46 |
47 | Reasoning process 48 |
49 |
50 | `; 51 | responseElement.insertBefore(reasoningContentElement, responseElement.firstChild); 52 | } 53 | 54 | const reasoningInner = reasoningContentElement.querySelector('.reasoning-content-inner'); 55 | if (reasoningInner) { 56 | const reasoningHtml = await md.render(currentChunk.reasoningContent); 57 | reasoningInner.innerHTML = reasoningHtml; 58 | } 59 | } 60 | 61 | // 获取或创建content容器 62 | if (currentChunk.content) { 63 | let contentElement = responseElement.querySelector('.content-container'); 64 | if (!contentElement) { 65 | contentElement = document.createElement('div'); 66 | contentElement.className = 'content-container'; 67 | responseElement.appendChild(contentElement); 68 | } 69 | 70 | const contentHtml = await md.render(currentChunk.content); 71 | contentElement.innerHTML = contentHtml; 72 | } 73 | 74 | // 使用requestAnimationFrame优化滚动和更新 75 | if (getAllowAutoScroll() && aiResponseContainer.isConnected) { 76 | requestAnimationFrame(() => { 77 | scrollToBottom(aiResponseContainer); 78 | if (ps?.update) ps.update(); 79 | }); 80 | } 81 | } catch (error) { 82 | console.error('Error processing render queue:', error); 83 | } 84 | } 85 | 86 | // 验证和清理消息历史 87 | function validateAndCleanMessages() { 88 | // 如果发现连续的user消息,删除前一条 89 | for (let i = messages.length - 1; i > 0; i--) { 90 | if (messages[i].role === 'user' && messages[i-1].role === 'user') { 91 | messages.splice(i-1, 1); 92 | } 93 | } 94 | } 95 | 96 | export async function getAIResponse( 97 | text, 98 | responseElement, 99 | signal, 100 | ps, 101 | iconContainer, 102 | aiResponseContainer, 103 | isRefresh = false, 104 | onComplete, 105 | isGreeting = false, 106 | quickActionPrompt = '', 107 | onGenerationComplete = null, 108 | onGenerationError = null 109 | ) { 110 | if (!text) return; 111 | 112 | isGenerating = true; 113 | window.currentAbortController = signal?.controller || new AbortController(); 114 | 115 | // 设置中止信号处理 116 | window.currentAbortController.signal.addEventListener('abort', () => { 117 | // 发送中止请求消息到background 118 | chrome.runtime.sendMessage({ action: "abortRequest" }); 119 | }); 120 | 121 | if (isRefresh) { 122 | messages = messages.slice(0, -1); 123 | } 124 | 125 | validateAndCleanMessages(); 126 | if (!isRefresh) { 127 | messages.push({ role: "user", content: text }); 128 | } 129 | 130 | const existingIconContainer = responseElement.querySelector('.icon-container'); 131 | const originalClassName = responseElement.className; 132 | responseElement.textContent = ""; 133 | if (existingIconContainer) { 134 | responseElement.appendChild(existingIconContainer); 135 | } 136 | responseElement.className = originalClassName; 137 | 138 | try { 139 | const settings = await new Promise(resolve => { 140 | chrome.runtime.sendMessage({ action: "getSettings" }, resolve); 141 | }); 142 | 143 | const provider = settings.provider || 'deepseek'; 144 | let apiKey = ''; 145 | 146 | // 根据provider选择Api Key 147 | if (provider.startsWith('custom_')) { 148 | // 对于自定义服务商,API key可能来自两个地方 149 | // 1. settings.customApiKey (background.js已经处理过) 150 | // 2. 如果customApiKey为空,尝试从customProviders数组中获取 151 | apiKey = settings.customApiKey; 152 | 153 | // 如果从background获取的customApiKey为空,尝试从customProviders中获取 154 | if (!apiKey && settings.customProviders) { 155 | const customProvider = settings.customProviders.find(p => p.id === provider); 156 | if (customProvider && customProvider.apiKey) { 157 | apiKey = customProvider.apiKey; 158 | } 159 | } 160 | } else { 161 | apiKey = provider === 'siliconflow' ? settings.siliconflowApiKey : 162 | provider === 'openrouter' ? settings.openrouterApiKey : 163 | provider === 'volcengine' ? settings.volcengineApiKey : 164 | provider === 'tencentcloud' ? settings.tencentcloudApiKey : 165 | provider === 'iflytekstar' ? settings.iflytekstarApiKey : 166 | provider === 'baiducloud' ? settings.baiducloudApiKey : 167 | provider === 'aliyun' ? settings.aliyunApiKey : 168 | provider === 'aihubmix' ? settings.aihubmixApiKey : 169 | settings.deepseekApiKey; 170 | } 171 | 172 | const language = settings.language; 173 | let model = settings.model; 174 | 175 | // 获取服务商和模型的显示名称 176 | let providerDisplayName = provider; 177 | let modelDisplayName = model; 178 | 179 | // 如果是自定义Provider,获取显示名称 180 | if (provider.startsWith('custom_') && settings.customProviders) { 181 | const customProvider = settings.customProviders.find(p => p.id === provider); 182 | if (customProvider) { 183 | // 获取显示名称 184 | providerDisplayName = customProvider.name || provider; 185 | 186 | // 如果需要,从自定义服务商中获取模型名称 187 | if (customProvider.modelName && !model) { 188 | model = customProvider.modelName; 189 | } 190 | } 191 | } 192 | 193 | // 异步获取模型显示名称 194 | if (provider.startsWith('custom_') && model) { 195 | // 尝试从storage获取模型列表 196 | const modelStorageKey = `${provider}Models`; 197 | const modelData = await new Promise(resolve => { 198 | chrome.storage.sync.get(modelStorageKey, resolve); 199 | }); 200 | 201 | if (modelData && modelData[modelStorageKey]) { 202 | const models = modelData[modelStorageKey]; 203 | const foundModel = models.find(m => m.value === model); 204 | if (foundModel && foundModel.label) { 205 | modelDisplayName = foundModel.label; 206 | } 207 | } 208 | } 209 | 210 | // 自定义模型优先级处理 211 | // console.log(`🔍 使用模型: ${modelDisplayName}, Provider: ${providerDisplayName}`); 212 | 213 | // 获取自定义API URL 214 | let customApiUrl = ''; 215 | 216 | if (provider.startsWith('custom_')) { 217 | customApiUrl = settings.customApiUrl; 218 | } else { 219 | customApiUrl = provider === 'siliconflow' ? settings.siliconflowCustomApiUrl : 220 | provider === 'openrouter' ? settings.openrouterCustomApiUrl : 221 | provider === 'volcengine' ? settings.volcengineCustomApiUrl : 222 | provider === 'tencentcloud' ? settings.tencentcloudCustomApiUrl : 223 | provider === 'iflytekstar' ? settings.iflytekstarCustomApiUrl : 224 | provider === 'baiducloud' ? settings.baiducloudCustomApiUrl : 225 | provider === 'aliyun' ? settings.aliyunCustomApiUrl : 226 | provider === 'aihubmix' ? settings.aihubmixCustomApiUrl : 227 | settings.deepseekCustomApiUrl; 228 | } 229 | 230 | if (!apiKey) { 231 | const linkElement = document.createElement("a"); 232 | linkElement.href = "#"; 233 | linkElement.textContent = "Please first set your API key in extension popup."; 234 | linkElement.style.color = "#0066cc"; 235 | linkElement.style.textDecoration = "underline"; 236 | linkElement.style.cursor = "pointer"; 237 | linkElement.addEventListener("click", async (e) => { 238 | e.preventDefault(); 239 | try { 240 | await chrome.runtime.sendMessage({ action: "openPopup" }); 241 | } catch (error) { 242 | console.error('Failed to open popup:', error); 243 | chrome.runtime.sendMessage({ action: "getSelectedText" }); 244 | } 245 | }); 246 | responseElement.textContent = ""; 247 | responseElement.appendChild(linkElement); 248 | if (existingIconContainer) { 249 | responseElement.appendChild(existingIconContainer); 250 | } 251 | return; 252 | } 253 | 254 | // 使用自定义API URL或默认URL 255 | const apiUrl = customApiUrl || ( 256 | provider.startsWith('custom_') 257 | ? settings.customApiUrl // 自定义Provider直接使用其绑定的URL 258 | : provider === 'siliconflow' 259 | ? 'https://api.siliconflow.cn/v1/chat/completions' 260 | : provider === 'openrouter' 261 | ? 'https://openrouter.ai/api/v1/chat/completions' 262 | : provider === 'volcengine' 263 | ? 'https://ark.cn-beijing.volces.com/api/v3/chat/completions' 264 | : provider === 'tencentcloud' 265 | ? 'https://api.lkeap.cloud.tencent.com/v1/chat/completions' 266 | : provider === 'iflytekstar' 267 | ? 'https://maas-api.cn-huabei-1.xf-yun.com/v1/chat/completions' 268 | : provider === 'baiducloud' 269 | ? 'https://qianfan.baidubce.com/v2/chat/completions' 270 | : provider === 'aliyun' 271 | ? 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions' 272 | : provider === 'aihubmix' 273 | ? 'https://aihubmix.com/v1/chat/completions' 274 | : 'https://api.deepseek.com/v1/chat/completions' 275 | ); 276 | 277 | const modelName = isGreeting ? "deepseek-chat" : model; 278 | const modelDisplayNameForApi = isGreeting ? "deepseek-chat" : modelDisplayName; 279 | 280 | // 确保每次请求都使用正确的模型名称 281 | // console.log(`📦 调用API - 使用模型: ${modelDisplayNameForApi}`); 282 | 283 | const systemPrompt = quickActionPrompt && quickActionPrompt.includes('You are a professional multilingual translation engine') 284 | ? quickActionPrompt 285 | : language === "auto" 286 | ? "Detect and respond in the same language as the user's input. If the user's input is in Chinese, respond in Chinese. If the user's input is in English, respond in English, etc." 287 | : `You MUST respond ONLY in ${language}.Including your reasoningContent language. This is a strict requirement. Do not use any other language except ${language}.${quickActionPrompt || ''}`; 288 | 289 | const response = await new Promise((resolve, reject) => { 290 | let aiResponse = ""; 291 | let reasoningContent = ""; 292 | let aborted = false; 293 | 294 | window.currentAbortController.signal.addEventListener('abort', () => { 295 | aborted = true; 296 | resolve({ ok: true, content: aiResponse }); 297 | }); 298 | 299 | function handleResponse(response) { 300 | if (aborted) return; 301 | 302 | if (chrome.runtime.lastError) { 303 | const error = new Error(chrome.runtime.lastError.message || 'Chrome runtime error'); 304 | error.originalError = chrome.runtime.lastError; 305 | reject(error); 306 | return; 307 | } 308 | 309 | if (!response.ok) { 310 | const error = new Error(response.error || 'Request failed'); 311 | error.status = response.status; 312 | error.originalResponse = response; 313 | reject(error); 314 | return; 315 | } 316 | 317 | if (response.done) { 318 | resolve({ ok: true, content: aiResponse }); 319 | return; 320 | } 321 | 322 | try { 323 | const line = response.data; 324 | if (!line.startsWith("data: ")) return; 325 | 326 | const jsonLine = line.slice(6); 327 | if (jsonLine === "[DONE]") { 328 | resolve({ ok: true, content: aiResponse }); 329 | return; 330 | } 331 | 332 | const data = JSON.parse(jsonLine); 333 | 334 | requestAnimationFrame(() => { 335 | if (provider === 'openrouter' && data.choices?.[0]?.delta?.reasoning) { 336 | reasoningContent += data.choices[0].delta.reasoning; 337 | currentReasoningContent = reasoningContent; 338 | renderQueue = [{ 339 | reasoningContent, 340 | content: aiResponse 341 | }]; 342 | processRenderQueue(responseElement, ps, aiResponseContainer); 343 | } else if (data.choices?.[0]?.delta?.reasoning_content) { 344 | reasoningContent += data.choices[0].delta.reasoning_content; 345 | currentReasoningContent = reasoningContent; 346 | renderQueue = [{ 347 | reasoningContent, 348 | content: aiResponse 349 | }]; 350 | processRenderQueue(responseElement, ps, aiResponseContainer); 351 | } 352 | if (data.choices?.[0]?.delta?.content) { 353 | const content = data.choices[0].delta.content; 354 | aiResponse += content; 355 | currentContent = aiResponse; 356 | renderQueue = [{ 357 | reasoningContent: provider === 'openrouter' || model === "r1" ? reasoningContent : "", 358 | content: aiResponse 359 | }]; 360 | processRenderQueue(responseElement, ps, aiResponseContainer); 361 | } 362 | }); 363 | } catch (e) { 364 | console.error("Error parsing JSON:", e); 365 | } 366 | } 367 | 368 | const messageListener = (msg) => { 369 | if (msg.type === "streamResponse") { 370 | handleResponse(msg.response); 371 | if (msg.response.done) { 372 | chrome.runtime.onMessage.removeListener(messageListener); 373 | } 374 | } 375 | }; 376 | 377 | chrome.runtime.onMessage.addListener(messageListener); 378 | 379 | // console.log(`🚀 发送请求 - 服务商: ${providerDisplayName}, 模型: ${modelDisplayNameForApi},请求体:${apiUrl}`); 380 | 381 | // 确保请求中包含正确的模型名称 382 | const requestBody = { 383 | model: modelName, 384 | messages: [ 385 | { role: "system", content: systemPrompt }, 386 | ...messages 387 | ], 388 | stream: true, 389 | ...(provider === 'openrouter' && { include_reasoning: true }) 390 | }; 391 | 392 | // console.log(`📦 请求数据 - 模型: ${modelDisplayNameForApi}`); 393 | 394 | chrome.runtime.sendMessage({ 395 | action: "proxyRequest", 396 | url: apiUrl, 397 | method: "POST", 398 | headers: { 399 | "Content-Type": "application/json", 400 | Authorization: `Bearer ${apiKey}`, 401 | }, 402 | body: JSON.stringify(requestBody) 403 | }); 404 | }); 405 | 406 | if (currentContent) { 407 | messages.push({ role: "assistant", content: currentContent }); 408 | } 409 | requestIdleCallback(() => { 410 | if (window.addIconsToElement) { 411 | window.addIconsToElement(responseElement); 412 | } 413 | if (window.updateLastAnswerIcons) { 414 | window.updateLastAnswerIcons(); 415 | } 416 | }, { timeout: 1000 }); 417 | 418 | if (iconContainer) { 419 | iconContainer.style.display = 'flex'; 420 | iconContainer.dataset.initialShow = 'true'; 421 | 422 | const observer = new IntersectionObserver((entries) => { 423 | entries.forEach(entry => { 424 | if (entry.isIntersecting) { 425 | const buttonContainer = responseElement.querySelector('.icon-container'); 426 | if (buttonContainer && aiResponseContainer) { 427 | const buttonRect = buttonContainer.getBoundingClientRect(); 428 | const containerRect = aiResponseContainer.getBoundingClientRect(); 429 | const buttonBottom = buttonRect.bottom - containerRect.top; 430 | 431 | if (buttonBottom > aiResponseContainer.clientHeight) { 432 | const extraScroll = buttonBottom - aiResponseContainer.clientHeight + 40; 433 | aiResponseContainer.scrollTop += extraScroll; 434 | if (ps) ps.update(); 435 | } 436 | } 437 | observer.disconnect(); 438 | } 439 | }); 440 | }); 441 | 442 | observer.observe(iconContainer); 443 | } 444 | 445 | if (onComplete) { 446 | requestIdleCallback(() => onComplete(), { timeout: 1000 }); 447 | } 448 | 449 | if (onGenerationComplete) { 450 | onGenerationComplete(); 451 | } 452 | } catch (error) { 453 | console.error("Error:", error); 454 | 455 | if (error.name !== 'AbortError') { 456 | // 使用handleError函数处理错误,传递原始错误信息 457 | const errorData = error.originalResponse || error.originalError || error; 458 | const errorStatus = error.status || (error.originalResponse?.status) || 500; 459 | handleError(errorStatus, responseElement, errorData, onGenerationError); 460 | } 461 | } finally { 462 | isGenerating = false; 463 | window.currentAbortController = null; 464 | 465 | // 显示代码块复制按钮 466 | requestAnimationFrame(() => { 467 | showCodeCopyButtons(); 468 | }); 469 | } 470 | } 471 | 472 | function handleError(status, responseElement, errorInfo, onGenerationError) { 473 | isGenerating = false; 474 | renderQueue = []; 475 | 476 | // 优先使用API返回的原始错误信息 477 | let errorMessage = "Failed to connect to AI service."; 478 | 479 | if (errorInfo && typeof errorInfo === 'object') { 480 | // 如果有API返回的详细错误信息,优先使用 481 | if (errorInfo.message) { 482 | errorMessage = errorInfo.message; 483 | } else if (errorInfo.error && errorInfo.error.message) { 484 | errorMessage = errorInfo.error.message; 485 | } else if (typeof errorInfo.error === 'string') { 486 | errorMessage = errorInfo.error; 487 | } 488 | } else if (errorInfo && typeof errorInfo === 'string') { 489 | // 如果错误信息是字符串,直接使用 490 | errorMessage = errorInfo; 491 | } else { 492 | // 只有在没有原始错误信息时才使用基于状态码的通用消息 493 | if (status === 401) { 494 | errorMessage = "API key is invalid or expired."; 495 | } else if (status === 429) { 496 | errorMessage = "Too many requests. Please slow down."; 497 | } else if (status === 500) { 498 | errorMessage = "AI service internal error. Please try again later."; 499 | } else if (status === 0) { 500 | errorMessage = "The request was cancelled or timed out."; 501 | } 502 | } 503 | 504 | responseElement.textContent = errorMessage; 505 | responseElement.classList.add('error'); // 添加错误状态类 506 | 507 | // 调用错误回调 508 | if (typeof onGenerationError === 'function') { 509 | onGenerationError(errorMessage); 510 | } 511 | } -------------------------------------------------------------------------------- /src/content/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const SCROLL_CONSTANTS = { 2 | SCROLL_THRESHOLD: 30, // 滚动触发阈值 3 | COOLDOWN_DURATION: 150, // 滚动冷却时间(毫秒) 4 | ANIMATION_DURATION: 300, // 动画持续时间(毫秒) 5 | VELOCITY_THRESHOLD: 0.5, // 速度阈值 6 | MAX_MOMENTUM_SAMPLES: 5, // 最大动量采样数 7 | BUTTON_SPACE: 70 // 按钮预留空间 8 | }; 9 | 10 | export const STYLE_CONSTANTS = { 11 | DEFAULT_POPUP_WIDTH: '580px', 12 | DEFAULT_POPUP_HEIGHT: '380px', 13 | DEFAULT_PADDING_TOP: '20px', 14 | MIN_WIDTH: 300, 15 | MAX_WIDTH: 900, 16 | MIN_HEIGHT: 200, 17 | MAX_HEIGHT: 800 18 | }; 19 | 20 | export const THEME_CLASSES = { 21 | LIGHT: 'light-mode', 22 | DARK: 'dark-mode' 23 | }; -------------------------------------------------------------------------------- /src/content/utils/markdownRenderer.js: -------------------------------------------------------------------------------- 1 | import MarkdownIt from "markdown-it"; 2 | import hljs from "highlight.js"; 3 | import mathjax3 from "markdown-it-mathjax3"; 4 | 5 | // 使用 WeakMap 来缓存已处理过的数学公式 6 | const mathCache = new WeakMap(); 7 | const processedTexts = new Map(); 8 | 9 | // 使用 Memoization 优化预处理数学公式 10 | const memoizedPreprocessMath = (() => { 11 | const cache = new Map(); 12 | return (text) => { 13 | if (cache.has(text)) { 14 | return cache.get(text); 15 | } 16 | const result = preprocessMath(text); 17 | cache.set(text, result); 18 | return result; 19 | }; 20 | })(); 21 | 22 | // 预处理数学公式 23 | function preprocessMath(text) { 24 | // 使用正则表达式优化:减少重复处理 25 | const patterns = { 26 | brackets: /[{([})\]]/g, 27 | blockFormula: /\\\[([\s\S]*?)\\\]/g, 28 | inlineFormula: /\\\(([\s\S]*?)\\\)/g, 29 | subscripts: /(\d+|[a-zA-Z])([_^])(\d+)(?!\})/g, 30 | specialSymbols: /\\(pm|mp|times|div|gamma|ln|int|infty|leq|geq|neq|approx)\b/g, 31 | }; 32 | 33 | // 优化括号匹配检查 34 | const checkBrackets = (str) => { 35 | const stack = []; 36 | const pairs = { '{': '}', '[': ']', '(': ')' }; 37 | for (const char of str.match(patterns.brackets) || []) { 38 | if ('{(['.includes(char)) { 39 | stack.push(char); 40 | } else if (stack.length === 0 || pairs[stack.pop()] !== char) { 41 | return false; 42 | } 43 | } 44 | return stack.length === 0; 45 | }; 46 | 47 | // 批量处理文本替换 48 | let processed = text 49 | .replace(/\n{3,}/g, '\n\n') 50 | .replace(/[ \t]+$/gm, ''); 51 | 52 | // 优化块级公式处理 53 | processed = processed.replace(patterns.blockFormula, (_, p1) => 54 | `\n$$${p1.trim().replace(/\n\s+/g, '\n')}$$\n` 55 | ); 56 | 57 | // 优化行内公式处理 58 | processed = processed.replace(patterns.inlineFormula, (_, p1) => 59 | `$${p1.trim()}$` 60 | ); 61 | 62 | // 优化上下标处理 63 | processed = processed.replace(patterns.subscripts, '$1$2{$3}'); 64 | 65 | // 使用 Map 优化特殊字符替换 66 | const specialChars = new Map([ 67 | ['∫', '\\int '], 68 | ['±', '\\pm '], 69 | ['∓', '\\mp '], 70 | ['×', '\\times '], 71 | ['÷', '\\div '], 72 | ['∞', '\\infty '], 73 | ['≤', '\\leq '], 74 | ['≥', '\\geq '], 75 | ['≠', '\\neq '], 76 | ['≈', '\\approx '] 77 | ]); 78 | 79 | // 批量处理特殊字符 80 | for (const [char, replacement] of specialChars) { 81 | processed = processed.replaceAll(char, replacement); 82 | } 83 | 84 | return processed; 85 | } 86 | 87 | // 创建 MarkdownIt 实例并优化配置 88 | const md = new MarkdownIt({ 89 | html: true, 90 | linkify: true, 91 | typographer: true, 92 | highlight: (str, lang) => { 93 | if (!lang || !hljs.getLanguage(lang)) { 94 | return `
${md.utils.escapeHtml(str)}
`; 95 | } 96 | try { 97 | return `
${hljs.highlight(str, { language: lang }).value}
`; 98 | } catch { 99 | return `
${md.utils.escapeHtml(str)}
`; 100 | } 101 | } 102 | }); 103 | 104 | // 优化 mathjax 配置 105 | const mathjaxOptions = { 106 | tex: { 107 | inlineMath: [['$', '$']], 108 | displayMath: [['$$', '$$']], 109 | processEscapes: true, 110 | processEnvironments: true, 111 | packages: ['base', 'ams', 'noerrors', 'noundefined'] 112 | }, 113 | options: { 114 | skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'], 115 | ignoreHtmlClass: 'tex2jax_ignore', 116 | processHtmlClass: 'tex2jax_process' 117 | }, 118 | chtml: { 119 | scale: 1, 120 | minScale: .5, 121 | mtextInheritFont: true, 122 | merrorInheritFont: true 123 | } 124 | }; 125 | 126 | md.use(mathjax3, mathjaxOptions); 127 | 128 | // 优化渲染方法 129 | const originalRender = md.render.bind(md); 130 | md.render = function(text) { 131 | try { 132 | // 使用缓存的预处理结果 133 | const preprocessedText = memoizedPreprocessMath(text); 134 | 135 | if (processedTexts.has(preprocessedText)) { 136 | return processedTexts.get(preprocessedText); 137 | } 138 | 139 | // 使用 Promise 和 requestAnimationFrame 优化渲染时机 140 | const renderPromise = new Promise((resolve) => { 141 | requestAnimationFrame(() => { 142 | const result = originalRender(preprocessedText) 143 | .replace(/\$\$([\s\S]+?)\$\$/g, (_, p1) => 144 | `
$$${p1}$$
` 145 | ) 146 | .replace(/\$([^$]+?)\$/g, (_, p1) => 147 | `$${p1}$` 148 | ); 149 | 150 | processedTexts.set(preprocessedText, result); 151 | resolve(result); 152 | }); 153 | }); 154 | 155 | return renderPromise; 156 | } catch (error) { 157 | console.error('渲染错误:', error); 158 | return originalRender(text); 159 | } 160 | }; 161 | 162 | // 优化代码块渲染器 163 | md.renderer.rules.fence = (() => { 164 | const defaultFence = md.renderer.rules.fence; 165 | 166 | return function(tokens, idx, options, env, self) { 167 | const token = tokens[idx]; 168 | const code = token.content.trim(); 169 | const lang = token.info.trim(); 170 | 171 | const rawHtml = defaultFence(tokens, idx, options, env, self); 172 | 173 | return ` 174 |
175 |
${rawHtml}
176 | 179 |
180 | `.trim(); 181 | }; 182 | })(); 183 | 184 | // 使用事件委托处理复制按钮点击 185 | document.addEventListener('click', async function(event) { 186 | const copyButton = event.target.closest('.copy-button'); 187 | if (!copyButton) return; 188 | 189 | event.preventDefault(); 190 | event.stopPropagation(); 191 | 192 | const code = decodeURIComponent(copyButton.dataset.code); 193 | if (code) { 194 | try { 195 | await navigator.clipboard.writeText(code); 196 | 197 | // 复制成功的视觉反馈 198 | copyButton.classList.add('copied'); 199 | 200 | // 更新复制按钮文本/图标 201 | const copyIcon = copyButton.querySelector('.copy-icon'); 202 | if (copyIcon) { 203 | // 使用check.svg图标 204 | copyButton.innerHTML = `Copied`; 205 | } 206 | 207 | // 2秒后恢复原始状态 208 | setTimeout(() => { 209 | copyButton.classList.remove('copied'); 210 | copyButton.innerHTML = `Copy`; 211 | }, 2000); 212 | 213 | } catch (error) { 214 | console.error('Failed to copy:', error); 215 | } 216 | } else { 217 | console.warn('No code text found to copy'); 218 | } 219 | }, true); 220 | 221 | // 添加全局处理函数,在AI响应完成后才显示复制按钮 222 | export function initCopyButtonsVisibility() { 223 | // 监听DOM变化,处理流式响应过程中的代码块 224 | const observer = new MutationObserver((mutations) => { 225 | mutations.forEach(mutation => { 226 | if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 227 | mutation.addedNodes.forEach(node => { 228 | if (node.nodeType === Node.ELEMENT_NODE) { 229 | // 查找新添加的代码块 230 | const codeBlocks = node.querySelectorAll ? node.querySelectorAll('.code-block-wrapper') : []; 231 | codeBlocks.forEach(block => { 232 | // 隐藏复制按钮直到代码完成加载 233 | const button = block.querySelector('.copy-button'); 234 | if (button) { 235 | button.style.display = 'none'; 236 | } 237 | }); 238 | } 239 | }); 240 | } 241 | }); 242 | }); 243 | 244 | // 观察AI响应容器 245 | const aiResponseContainer = document.getElementById('ai-response'); 246 | if (aiResponseContainer) { 247 | observer.observe(aiResponseContainer, { 248 | childList: true, 249 | subtree: true 250 | }); 251 | } 252 | 253 | return observer; 254 | } 255 | 256 | // 检查代码块是否已完成渲染(在AI响应完成后调用) 257 | export function showCodeCopyButtons() { 258 | const aiResponseContainer = document.getElementById('ai-response'); 259 | if (!aiResponseContainer) return; 260 | 261 | // 查找所有代码块中的复制按钮 262 | const copyButtons = aiResponseContainer.querySelectorAll('.code-block-wrapper .copy-button'); 263 | 264 | // 显示所有复制按钮 265 | copyButtons.forEach(button => { 266 | if (button.style.display === 'none') { 267 | // 先保持透明并逐渐淡入,避免突然出现 268 | button.style.opacity = '0'; 269 | button.style.display = ''; 270 | 271 | // 使用requestAnimationFrame确保样式变化已经应用 272 | requestAnimationFrame(() => { 273 | button.style.transition = 'opacity 0.3s ease'; 274 | button.style.opacity = ''; 275 | }); 276 | } 277 | }); 278 | } 279 | 280 | export { md }; 281 | -------------------------------------------------------------------------------- /src/content/utils/popupStateManager.js: -------------------------------------------------------------------------------- 1 | // 管理弹窗状态的单例模块 2 | class PopupStateManager { 3 | constructor() { 4 | this.isCreatingPopup = false; 5 | this.isPopupVisible = false; 6 | } 7 | 8 | setCreating(value) { 9 | this.isCreatingPopup = value; 10 | } 11 | 12 | setVisible(value) { 13 | this.isPopupVisible = value; 14 | } 15 | 16 | isCreating() { 17 | return this.isCreatingPopup; 18 | } 19 | 20 | isVisible() { 21 | return this.isPopupVisible; 22 | } 23 | 24 | reset() { 25 | this.isCreatingPopup = false; 26 | this.isPopupVisible = false; 27 | } 28 | } 29 | 30 | // 导出单例实例 31 | export const popupStateManager = new PopupStateManager(); -------------------------------------------------------------------------------- /src/content/utils/scrollManager.js: -------------------------------------------------------------------------------- 1 | import { SCROLL_CONSTANTS } from './constants'; 2 | import { getIsGenerating } from '../services/apiService'; 3 | 4 | class ScrollStateManager { 5 | constructor() { 6 | this.isManualScrolling = false; 7 | this.lastScrollTime = 0; 8 | this.scrollTimeout = null; 9 | this.scrollRAF = null; 10 | this.scrollAnimation = { 11 | isAnimating: false, 12 | startTime: 0, 13 | startPosition: 0, 14 | targetPosition: 0, 15 | duration: SCROLL_CONSTANTS.ANIMATION_DURATION, 16 | }; 17 | this.scrollMomentum = { 18 | velocity: 0, 19 | timestamp: 0, 20 | positions: [], 21 | maxSamples: SCROLL_CONSTANTS.MAX_MOMENTUM_SAMPLES, 22 | }; 23 | } 24 | 25 | setManualScrolling(value) { 26 | this.isManualScrolling = value; 27 | if (value) { 28 | this.lastScrollTime = Date.now(); 29 | this.scrollAnimation.isAnimating = false; 30 | } 31 | } 32 | 33 | updateScrollVelocity(currentPosition) { 34 | const now = Date.now(); 35 | const positions = this.scrollMomentum.positions; 36 | 37 | positions.push({ 38 | position: currentPosition, 39 | timestamp: now 40 | }); 41 | 42 | if (positions.length > this.scrollMomentum.maxSamples) { 43 | positions.shift(); 44 | } 45 | 46 | if (positions.length >= 2) { 47 | const newest = positions[positions.length - 1]; 48 | const oldest = positions[0]; 49 | const timeDiff = newest.timestamp - oldest.timestamp; 50 | 51 | if (timeDiff > 0) { 52 | this.scrollMomentum.velocity = (newest.position - oldest.position) / timeDiff; 53 | } 54 | } 55 | } 56 | 57 | isRapidScrolling() { 58 | return Math.abs(this.scrollMomentum.velocity) > 0.5; 59 | } 60 | 61 | isInCooldown() { 62 | return Date.now() - this.lastScrollTime < 150 || this.isRapidScrolling(); 63 | } 64 | 65 | saveScrollPosition(container) { 66 | const { scrollTop, scrollHeight, clientHeight } = container; 67 | const distanceFromBottom = scrollHeight - (scrollTop + clientHeight); 68 | this.isAtBottom = distanceFromBottom <= SCROLL_CONSTANTS.SCROLL_THRESHOLD; 69 | this.scrollPosition = scrollTop; 70 | this.updateScrollVelocity(scrollTop); 71 | } 72 | 73 | restoreScrollPosition(container) { 74 | if (this.isAtBottom || getIsGenerating()) { 75 | container.scrollTop = container.scrollHeight; 76 | } else { 77 | const scrollRatio = this.scrollPosition / container.scrollHeight; 78 | container.scrollTop = scrollRatio * container.scrollHeight; 79 | } 80 | } 81 | 82 | isNearBottom(container) { 83 | const { scrollTop, scrollHeight, clientHeight } = container; 84 | return scrollHeight - (scrollTop + clientHeight) <= SCROLL_CONSTANTS.SCROLL_THRESHOLD; 85 | } 86 | 87 | cleanup() { 88 | if (this.scrollTimeout) { 89 | clearTimeout(this.scrollTimeout); 90 | } 91 | if (this.scrollRAF) { 92 | cancelAnimationFrame(this.scrollRAF); 93 | } 94 | this.scrollAnimation.isAnimating = false; 95 | this.scrollMomentum.positions = []; 96 | this.scrollMomentum.velocity = 0; 97 | } 98 | } 99 | 100 | let allowAutoScroll = true; 101 | 102 | export function setAllowAutoScroll(value) { 103 | allowAutoScroll = value; 104 | } 105 | 106 | export function getAllowAutoScroll() { 107 | return allowAutoScroll; 108 | } 109 | 110 | export function updateAllowAutoScroll(container) { 111 | if (!container) return; 112 | 113 | const { scrollTop, scrollHeight, clientHeight } = container; 114 | const EXTRA_SCROLL_SPACE = 40; // 与上面相同的额外空间 115 | const isAtBottom = scrollHeight - (scrollTop + clientHeight + EXTRA_SCROLL_SPACE) < SCROLL_CONSTANTS.SCROLL_THRESHOLD; 116 | setAllowAutoScroll(isAtBottom); 117 | } 118 | 119 | export function handleUserScroll(event) { 120 | if (!event || !event.target) return; 121 | 122 | const container = event.target; 123 | if (!container) return; 124 | 125 | requestAnimationFrame(() => { 126 | updateAllowAutoScroll(container); 127 | }); 128 | } 129 | 130 | export function scrollToBottom(container) { 131 | if (!container) return; 132 | 133 | requestAnimationFrame(() => { 134 | container.scrollTop = container.scrollHeight; 135 | 136 | if (container.perfectScrollbar) { 137 | container.perfectScrollbar.update(); 138 | } 139 | }); 140 | } 141 | 142 | export function createScrollManager() { 143 | const scrollState = { 144 | isManualScrolling: false, 145 | lastScrollTime: 0, 146 | scrollTimeout: null, 147 | isAtBottom: true, 148 | scrollPosition: 0, 149 | scrollMomentum: { 150 | positions: [], 151 | maxSamples: 5, 152 | velocity: 0 153 | } 154 | }; 155 | 156 | return { 157 | ...scrollState, 158 | 159 | setManualScrolling(value) { 160 | this.isManualScrolling = value; 161 | if (value) { 162 | this.lastScrollTime = Date.now(); 163 | } 164 | }, 165 | 166 | updateScrollVelocity(currentPosition) { 167 | const now = Date.now(); 168 | const positions = this.scrollMomentum.positions; 169 | 170 | positions.push({ 171 | position: currentPosition, 172 | timestamp: now 173 | }); 174 | 175 | if (positions.length > this.scrollMomentum.maxSamples) { 176 | positions.shift(); 177 | } 178 | 179 | if (positions.length >= 2) { 180 | const newest = positions[positions.length - 1]; 181 | const oldest = positions[0]; 182 | const timeDiff = newest.timestamp - oldest.timestamp; 183 | 184 | if (timeDiff > 0) { 185 | this.scrollMomentum.velocity = (newest.position - oldest.position) / timeDiff; 186 | } 187 | } 188 | }, 189 | 190 | isRapidScrolling() { 191 | return Math.abs(this.scrollMomentum.velocity) > 0.5; 192 | }, 193 | 194 | isInCooldown() { 195 | return Date.now() - this.lastScrollTime < 150 || this.isRapidScrolling(); 196 | }, 197 | 198 | isNearBottom(container) { 199 | if (!container) return false; 200 | const { scrollTop, scrollHeight, clientHeight } = container; 201 | return scrollHeight - (scrollTop + clientHeight) <= 30; 202 | }, 203 | 204 | saveScrollPosition(container) { 205 | if (!container) return; 206 | const { scrollTop, scrollHeight, clientHeight } = container; 207 | this.isAtBottom = scrollHeight - (scrollTop + clientHeight) <= 30; 208 | this.scrollPosition = scrollTop; 209 | this.updateScrollVelocity(scrollTop); 210 | }, 211 | 212 | restoreScrollPosition(container) { 213 | if (!container) return; 214 | if (this.isAtBottom || getIsGenerating()) { 215 | scrollToBottom(container); 216 | } else { 217 | container.scrollTop = this.scrollPosition; 218 | if (container.perfectScrollbar) { 219 | container.perfectScrollbar.update(); 220 | } 221 | } 222 | }, 223 | 224 | scrollToBottom(container) { 225 | scrollToBottom(container); 226 | }, 227 | 228 | cleanup() { 229 | if (this.scrollTimeout) { 230 | clearTimeout(this.scrollTimeout); 231 | } 232 | this.isManualScrolling = false; 233 | this.lastScrollTime = 0; 234 | this.scrollTimeout = null; 235 | this.scrollMomentum.positions = []; 236 | this.scrollMomentum.velocity = 0; 237 | } 238 | }; 239 | } 240 | 241 | // 优化的滚动处理函数 242 | export function setupScrollHandlers(container, perfectScrollbar) { 243 | if (!container) return; 244 | 245 | const scrollManager = container.scrollStateManager; 246 | 247 | const handleScroll = (event) => { 248 | if (!scrollManager) return; 249 | 250 | scrollManager.setManualScrolling(true); 251 | scrollManager.saveScrollPosition(container); 252 | 253 | handleUserScroll(event); 254 | 255 | requestAnimationFrame(() => { 256 | if (perfectScrollbar) { 257 | perfectScrollbar.update(); 258 | } 259 | 260 | if (!scrollManager.isInCooldown()) { 261 | const isAtBottom = scrollManager.isNearBottom(container); 262 | updateAllowAutoScroll(container); 263 | 264 | if (isAtBottom) { 265 | scrollManager.scrollToBottom(container); 266 | } 267 | } 268 | }); 269 | 270 | // 重置手动滚动状态 271 | if (scrollManager.scrollTimeout) { 272 | clearTimeout(scrollManager.scrollTimeout); 273 | } 274 | scrollManager.scrollTimeout = setTimeout(() => { 275 | scrollManager.setManualScrolling(false); 276 | }, 150); 277 | }; 278 | 279 | // 使用 passive 选项优化性能 280 | container.addEventListener('wheel', handleScroll, { passive: true }); 281 | container.addEventListener('touchstart', handleScroll, { passive: true }); 282 | container.addEventListener('touchmove', handleScroll, { passive: true }); 283 | container.addEventListener('scroll', handleScroll, { passive: true }); 284 | 285 | // 返回清理函数 286 | return () => { 287 | container.removeEventListener('wheel', handleScroll); 288 | container.removeEventListener('touchstart', handleScroll); 289 | container.removeEventListener('touchmove', handleScroll); 290 | container.removeEventListener('scroll', handleScroll); 291 | }; 292 | } -------------------------------------------------------------------------------- /src/content/utils/themeManager.js: -------------------------------------------------------------------------------- 1 | import { THEME_CLASSES } from './constants'; 2 | 3 | /** 4 | * 判断是否为暗色模式 5 | * 增强版色彩分析,支持网站特异性检测 6 | */ 7 | export function isDarkMode() { 8 | // 1. 检查特定网站的主题实现 9 | const specificTheme = detectSpecificSiteTheme(); 10 | if (specificTheme !== null) { 11 | return specificTheme; 12 | } 13 | 14 | // 2. 检查用户手动设置的主题偏好 15 | const userPreference = getUserThemePreference(); 16 | if (userPreference !== 'auto') { 17 | return userPreference === 'dark'; 18 | } 19 | 20 | // 3. 检查CSS变量定义的主题 21 | const cssVarTheme = detectCSSVarTheme(); 22 | if (cssVarTheme !== null) { 23 | return cssVarTheme; 24 | } 25 | 26 | // 4. 分析页面颜色 27 | return analyzePageColors(); 28 | } 29 | 30 | /** 31 | * 分析页面颜色判断主题 32 | */ 33 | function analyzePageColors() { 34 | const bodyBg = window.getComputedStyle(document.body).backgroundColor; 35 | const htmlBg = window.getComputedStyle(document.documentElement).backgroundColor; 36 | const bodyColor = window.getComputedStyle(document.body).color; 37 | const htmlColor = window.getComputedStyle(document.documentElement).color; 38 | 39 | function parseColor(color) { 40 | const match = color.match(/\d+/g); 41 | if (!match) return null; 42 | 43 | const [r, g, b, a = 255] = match.map(Number); 44 | const isTransparent = a === 0 || (r === 0 && g === 0 && b === 0 && a < 0.1); 45 | 46 | // 改进亮度计算,更符合人眼感知 47 | // 使用相对亮度公式: https://www.w3.org/TR/WCAG20/#relativeluminancedef 48 | const relativeLuminance = 49 | 0.2126 * (r / 255) + 50 | 0.7152 * (g / 255) + 51 | 0.0722 * (b / 255); 52 | 53 | return { 54 | r, g, b, a, 55 | isTransparent, 56 | brightness: relativeLuminance 57 | }; 58 | } 59 | 60 | const bodyBgColor = parseColor(bodyBg); 61 | const htmlBgColor = parseColor(htmlBg); 62 | const bodyTextColor = parseColor(bodyColor); 63 | const htmlTextColor = parseColor(htmlColor); 64 | 65 | // 优先使用背景色判断 66 | const effectiveBgColor = bodyBgColor?.isTransparent ? htmlBgColor : bodyBgColor; 67 | 68 | // 如果背景色无法判断,使用文字颜色 69 | const effectiveTextColor = bodyTextColor?.isTransparent ? htmlTextColor : bodyTextColor; 70 | 71 | if (effectiveBgColor && !effectiveBgColor.isTransparent) { 72 | return effectiveBgColor.brightness < 0.5; // 相对亮度小于0.5认为是暗色 73 | } 74 | 75 | // 如果背景色无法判断,通过文字颜色反推 76 | if (effectiveTextColor && !effectiveTextColor.isTransparent) { 77 | return effectiveTextColor.brightness > 0.5; // 文字亮意味着背景暗 78 | } 79 | 80 | // 如果都无法判断,使用系统主题 81 | return window.matchMedia('(prefers-color-scheme: dark)').matches; 82 | } 83 | 84 | /** 85 | * 检测特定网站的主题实现 86 | * @returns {boolean|null} - true表示暗色,false表示亮色,null表示无法判断 87 | */ 88 | function detectSpecificSiteTheme() { 89 | const host = window.location.hostname; 90 | 91 | // GitHub 92 | if (host.includes('github.com')) { 93 | const theme = document.documentElement.getAttribute('data-color-mode'); 94 | if (theme) { 95 | return theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches); 96 | } 97 | } 98 | 99 | // Google系产品 100 | if (host.includes('google.com') || host.includes('youtube.com')) { 101 | return document.documentElement.hasAttribute('dark') || document.documentElement.hasAttribute('darktheme'); 102 | } 103 | 104 | // Twitter/X 105 | if (host.includes('twitter.com') || host.includes('x.com')) { 106 | return document.documentElement.classList.contains('dark') || 107 | document.querySelector('html[data-theme="dark"]') !== null; 108 | } 109 | 110 | // 检查常见数据属性 111 | const dataTheme = document.documentElement.getAttribute('data-theme') || 112 | document.body.getAttribute('data-theme') || 113 | document.documentElement.getAttribute('data-color-mode') || 114 | document.body.getAttribute('data-color-mode'); 115 | 116 | if (dataTheme) { 117 | return dataTheme.includes('dark'); 118 | } 119 | 120 | // 检查常见类名 121 | if (document.documentElement.classList.contains('dark') || 122 | document.documentElement.classList.contains('darkTheme') || 123 | document.documentElement.classList.contains('dark-theme') || 124 | document.body.classList.contains('dark') || 125 | document.body.classList.contains('darkTheme') || 126 | document.body.classList.contains('dark-theme')) { 127 | return true; 128 | } 129 | 130 | if (document.documentElement.classList.contains('light') || 131 | document.documentElement.classList.contains('lightTheme') || 132 | document.documentElement.classList.contains('light-theme') || 133 | document.body.classList.contains('light') || 134 | document.body.classList.contains('lightTheme') || 135 | document.body.classList.contains('light-theme')) { 136 | return false; 137 | } 138 | 139 | return null; // 无法确定 140 | } 141 | 142 | /** 143 | * 检测基于CSS变量的主题 144 | */ 145 | function detectCSSVarTheme() { 146 | try { 147 | const styles = getComputedStyle(document.documentElement); 148 | 149 | // 检查常见的CSS变量 150 | const bgVar = styles.getPropertyValue('--background-color').trim() || 151 | styles.getPropertyValue('--bg-color').trim() || 152 | styles.getPropertyValue('--theme-background').trim(); 153 | 154 | const textVar = styles.getPropertyValue('--text-color').trim() || 155 | styles.getPropertyValue('--color-text').trim() || 156 | styles.getPropertyValue('--theme-text').trim(); 157 | 158 | if (bgVar) { 159 | const bgColor = parseSimpleColor(bgVar); 160 | if (bgColor) { 161 | return bgColor.isDark; 162 | } 163 | } 164 | 165 | if (textVar) { 166 | const textColor = parseSimpleColor(textVar); 167 | if (textColor) { 168 | return !textColor.isDark; // 文字深色表示浅色主题,反之亦然 169 | } 170 | } 171 | } catch (e) { 172 | console.debug('CSS变量分析出错', e); 173 | } 174 | 175 | return null; 176 | } 177 | 178 | /** 179 | * 简单解析颜色值 180 | */ 181 | function parseSimpleColor(color) { 182 | // 处理十六进制颜色 183 | if (color.startsWith('#')) { 184 | const hex = color.substring(1); 185 | if (hex.length === 3) { 186 | const r = parseInt(hex[0] + hex[0], 16); 187 | const g = parseInt(hex[1] + hex[1], 16); 188 | const b = parseInt(hex[2] + hex[2], 16); 189 | const brightness = (r * 299 + g * 587 + b * 114) / 1000 / 255; 190 | return { 191 | isDark: brightness < 0.5 192 | }; 193 | } else if (hex.length === 6) { 194 | const r = parseInt(hex.substring(0, 2), 16); 195 | const g = parseInt(hex.substring(2, 4), 16); 196 | const b = parseInt(hex.substring(4, 6), 16); 197 | const brightness = (r * 299 + g * 587 + b * 114) / 1000 / 255; 198 | return { 199 | isDark: brightness < 0.5 200 | }; 201 | } 202 | } 203 | 204 | // 处理rgb/rgba颜色 205 | const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/); 206 | if (rgbMatch) { 207 | const [_, r, g, b] = rgbMatch.map(Number); 208 | const brightness = (r * 299 + g * 587 + b * 114) / 1000 / 255; 209 | return { 210 | isDark: brightness < 0.5 211 | }; 212 | } 213 | 214 | return null; 215 | } 216 | 217 | /** 218 | * 获取用户主题偏好 219 | * @returns {'dark'|'light'|'auto'} 用户偏好 220 | */ 221 | function getUserThemePreference() { 222 | return localStorage.getItem('deepseek-theme-preference') || 'auto'; 223 | } 224 | 225 | /** 226 | * 设置用户主题偏好 227 | * @param {'dark'|'light'|'auto'} preference 用户偏好 228 | */ 229 | export function setUserThemePreference(preference) { 230 | if (['dark', 'light', 'auto'].includes(preference)) { 231 | localStorage.setItem('deepseek-theme-preference', preference); 232 | // 触发重新应用主题 233 | const isDark = preference === 'auto' ? isDarkMode() : (preference === 'dark'); 234 | document.dispatchEvent(new CustomEvent('deepseek-theme-change', { detail: { isDark }})); 235 | } 236 | } 237 | 238 | /** 239 | * 监视主题变化 240 | * 性能优化版本,减少不必要的检测 241 | */ 242 | export function watchThemeChanges(callback) { 243 | let currentTheme = isDarkMode(); 244 | 245 | // 高效防抖 246 | const debouncedCallback = debounce((isDark) => { 247 | if (currentTheme !== isDark) { 248 | currentTheme = isDark; 249 | callback(isDark); 250 | } 251 | }, 50); 252 | 253 | // 主题变化检测函数 254 | const checkThemeChange = () => { 255 | requestAnimationFrame(() => { 256 | const newTheme = isDarkMode(); 257 | debouncedCallback(newTheme); 258 | }); 259 | }; 260 | 261 | // 监听DOM变化,有选择性地过滤 262 | const observer = new MutationObserver((mutations) => { 263 | // 过滤掉不太可能影响主题的变化 264 | const shouldCheck = mutations.some(mutation => { 265 | // 类和主题相关属性的变化 266 | if (mutation.type === 'attributes') { 267 | const attr = mutation.attributeName; 268 | return attr === 'class' || 269 | attr === 'style' || 270 | attr === 'data-theme' || 271 | attr === 'data-color-mode' || 272 | attr === 'data-color-scheme' || 273 | attr.includes('theme') || 274 | attr.includes('mode') || 275 | attr.includes('dark') || 276 | attr.includes('light'); 277 | } 278 | return false; 279 | }); 280 | 281 | if (shouldCheck) { 282 | checkThemeChange(); 283 | } 284 | }); 285 | 286 | // 观察配置,减少不必要的触发 287 | const observerConfig = { 288 | attributes: true, 289 | attributeFilter: ['style', 'class', 'data-theme', 'data-color-mode', 'data-color-scheme', 'darktheme', 'dark'], 290 | subtree: false 291 | }; 292 | 293 | // 只监听HTML和BODY元素 294 | observer.observe(document.documentElement, observerConfig); 295 | observer.observe(document.body, observerConfig); 296 | 297 | // 监听系统主题变化 298 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 299 | const mediaQueryHandler = checkThemeChange; 300 | mediaQuery.addEventListener('change', mediaQueryHandler); 301 | 302 | // 监听自定义主题变化事件 303 | const customEventHandler = checkThemeChange; 304 | document.addEventListener('deepseek-theme-change', customEventHandler); 305 | 306 | // 初始化主题 307 | callback(currentTheme); 308 | 309 | // 返回清理函数 310 | return () => { 311 | observer.disconnect(); 312 | mediaQuery.removeEventListener('change', mediaQueryHandler); 313 | document.removeEventListener('deepseek-theme-change', customEventHandler); 314 | }; 315 | } 316 | 317 | /** 318 | * 高效防抖函数 319 | */ 320 | function debounce(func, wait) { 321 | let timeout; 322 | return function(...args) { 323 | clearTimeout(timeout); 324 | timeout = setTimeout(() => func(...args), wait); 325 | }; 326 | } 327 | 328 | /** 329 | * 应用主题到指定元素 330 | */ 331 | export function applyTheme(element, isDark) { 332 | if (!element) return; 333 | 334 | requestAnimationFrame(() => { 335 | if (isDark) { 336 | element.classList.add(THEME_CLASSES.DARK); 337 | element.classList.remove(THEME_CLASSES.LIGHT); 338 | element.setAttribute('data-theme', 'dark'); 339 | } else { 340 | element.classList.remove(THEME_CLASSES.DARK); 341 | element.classList.add(THEME_CLASSES.LIGHT); 342 | element.setAttribute('data-theme', 'light'); 343 | } 344 | }); 345 | } 346 | 347 | /** 348 | * 获取当前主题 349 | * @returns {'dark'|'light'} 当前主题 350 | */ 351 | export function getCurrentTheme() { 352 | return isDarkMode() ? 'dark' : 'light'; 353 | } -------------------------------------------------------------------------------- /src/icons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/.DS_Store -------------------------------------------------------------------------------- /src/icons/analyze.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/closeClicked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/explain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/hiddle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/icon128.png -------------------------------------------------------------------------------- /src/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/icon16.png -------------------------------------------------------------------------------- /src/icons/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/icon24.png -------------------------------------------------------------------------------- /src/icons/icon24.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/icon32.png -------------------------------------------------------------------------------- /src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/icon48.png -------------------------------------------------------------------------------- /src/icons/icon_copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/keyboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/icons/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/icons/logo.webp -------------------------------------------------------------------------------- /src/icons/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/redoClicked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/regenerate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/show.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/summarize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/translate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "DeepSeek AI", 4 | "description": "DeepSeek AI Assistant is a free and open-source browser extension tool (unrelated to DeepSeek official).", 5 | "version": "2.0.0", 6 | "permissions": ["storage", "contextMenus", "scripting", "commands", "tabs"], 7 | "content_scripts": [ 8 | { 9 | "matches": [""], 10 | "js": ["content.js"], 11 | "css": ["style.css"] 12 | } 13 | ], 14 | "background": { 15 | "service_worker": "background.js" 16 | }, 17 | "action": { 18 | "default_popup": "popup/popup.html", 19 | "default_icon": { 20 | "16": "icons/icon16.png", 21 | "48": "icons/icon48.png", 22 | "128": "icons/icon128.png" 23 | } 24 | }, 25 | "icons": { 26 | "16": "icons/icon16.png", 27 | "32": "icons/icon32.png", 28 | "48": "icons/icon48.png", 29 | "128": "icons/icon128.png" 30 | }, 31 | "web_accessible_resources": [ 32 | { 33 | "resources": [ 34 | "icons/icon16.png", 35 | "icons/icon24.png", 36 | "icons/icon32.png", 37 | "icons/icon48.png", 38 | "icons/icon128.png", 39 | "icons/copy.svg", 40 | "icons/close.svg", 41 | "icons/closeClicked.svg", 42 | "icons/regenerate.svg", 43 | "fonts/*", 44 | "Instructions.html", 45 | "instructions.js", 46 | "style.css" 47 | ], 48 | "matches": [""] 49 | }, 50 | { 51 | "resources": [ 52 | "icons/*.svg", 53 | "icons/*.png", 54 | "style.css" 55 | ], 56 | "matches": [""] 57 | } 58 | ], 59 | "content_security_policy": { 60 | "extension_pages": "script-src 'self'; object-src 'self'", 61 | "sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; object-src 'self'; font-src 'self' https://cdn.jsdelivr.net" 62 | }, 63 | "browser_specific_settings": { 64 | "edge": { 65 | "browser_action_next_to_addressbar": true 66 | } 67 | }, 68 | "commands": { 69 | "toggle-chat": { 70 | "suggested_key": { 71 | "default": "Ctrl+Shift+D", 72 | "mac": "Command+Shift+D", 73 | "windows": "Ctrl+Shift+D" 74 | }, 75 | "description": "Toggle the chat window." 76 | } 77 | }, 78 | "host_permissions": [ 79 | "" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /src/popup/EventManager.js: -------------------------------------------------------------------------------- 1 | export class EventManager { 2 | constructor(managers) { 3 | this.managers = managers; 4 | } 5 | 6 | // 初始化所有事件监听 7 | initializeEventListeners() { 8 | // API Key visibility toggle 9 | this.managers.uiManager.elements.toggleButton.addEventListener( 10 | "click", 11 | () => this.managers.uiManager.toggleApiKeyVisibility() 12 | ); 13 | 14 | // API Key focus event - 确保在输入时可见 15 | this.managers.uiManager.elements.apiKeyInput.addEventListener( 16 | "focus", 17 | () => { 18 | // 确保当获得焦点时内容可见 19 | this.managers.uiManager.elements.apiKeyInput.type = "text"; 20 | this.managers.uiManager.elements.iconSwitch.src = "../icons/hiddle.svg"; 21 | } 22 | ); 23 | 24 | // API Key validation and hiding on blur 25 | this.managers.uiManager.elements.apiKeyInput.addEventListener( 26 | "blur", 27 | () => { 28 | // 当有内容时,失去焦点时隐藏内容 29 | if (this.managers.uiManager.getApiKeyValue()) { 30 | this.managers.uiManager.elements.apiKeyInput.type = "password"; 31 | this.managers.uiManager.elements.iconSwitch.src = "../icons/show.svg"; 32 | } 33 | // 执行API验证 34 | this.managers.apiKeyManager.handleApiKeyValidation(); 35 | } 36 | ); 37 | 38 | // API Key input event - 确保在输入时可见 39 | this.managers.uiManager.elements.apiKeyInput.addEventListener( 40 | "input", 41 | () => { 42 | // 确保在输入时内容可见 43 | this.managers.uiManager.elements.apiKeyInput.type = "text"; 44 | this.managers.uiManager.elements.iconSwitch.src = "../icons/hiddle.svg"; 45 | } 46 | ); 47 | 48 | // 服务商切换事件 49 | this.managers.uiManager.elements.providerSelect.addEventListener( 50 | "change", 51 | async (e) => { 52 | const provider = e.target.value; 53 | 54 | try { 55 | // 保存服务商选择 56 | await this.managers.storageManager.saveProvider(provider); 57 | 58 | // 更新UI(包括API密钥和自定义API URL) 59 | await this.managers.providerUIManager.updateProviderUI(provider); 60 | 61 | // 更新模型选项 62 | await this.managers.modelManager.updateModelOptions(provider); 63 | } catch (error) { 64 | console.error(`服务商切换错误:`, error); 65 | } 66 | } 67 | ); 68 | 69 | // 自定义API URL保存 70 | this.managers.uiManager.elements.customApiUrlInput.addEventListener( 71 | "blur", 72 | () => this.managers.providerUIManager.handleCustomApiUrlSave() 73 | ); 74 | 75 | // 关闭模型弹窗按钮 76 | this.managers.uiManager.elements.closeModelModal?.addEventListener( 77 | "click", 78 | () => this.managers.uiManager.hideAddModelModal() 79 | ); 80 | 81 | // 取消添加模型按钮 82 | this.managers.uiManager.elements.cancelModelButton?.addEventListener( 83 | "click", 84 | () => this.managers.uiManager.hideAddModelModal() 85 | ); 86 | 87 | // 保存模型按钮 88 | this.managers.uiManager.elements.saveModelButton?.addEventListener( 89 | "click", 90 | () => this.managers.modelManager.handleSaveModel() 91 | ); 92 | 93 | // 自定义服务商API Key focus事件 94 | this.managers.uiManager.elements.customProviderApiKey?.addEventListener( 95 | "focus", 96 | () => { 97 | // 确保当获得焦点时内容可见 98 | this.managers.uiManager.elements.customProviderApiKey.type = "text"; 99 | } 100 | ); 101 | 102 | // 自定义服务商API Key input事件 103 | this.managers.uiManager.elements.customProviderApiKey?.addEventListener( 104 | "input", 105 | () => { 106 | // 确保在输入时内容可见 107 | this.managers.uiManager.elements.customProviderApiKey.type = "text"; 108 | } 109 | ); 110 | 111 | // 自定义服务商API Key blur事件 112 | this.managers.uiManager.elements.customProviderApiKey?.addEventListener( 113 | "blur", 114 | () => { 115 | // 当有内容时,失去焦点时隐藏内容 116 | if (this.managers.uiManager.getCustomProviderApiKey()) { 117 | this.managers.uiManager.elements.customProviderApiKey.type = "password"; 118 | } 119 | } 120 | ); 121 | 122 | // 关闭自定义服务商弹窗按钮 123 | this.managers.uiManager.elements.closeCustomProviderModal?.addEventListener( 124 | "click", 125 | () => this.managers.uiManager.hideCustomProviderModal() 126 | ); 127 | 128 | // 取消自定义服务商按钮 129 | this.managers.uiManager.elements.cancelCustomProviderButton?.addEventListener( 130 | "click", 131 | () => this.managers.uiManager.hideCustomProviderModal() 132 | ); 133 | 134 | // 保存自定义服务商按钮 135 | this.managers.uiManager.elements.saveCustomProviderButton?.addEventListener( 136 | "click", 137 | () => this.managers.providerUIManager.handleSaveCustomProvider() 138 | ); 139 | 140 | // 点击弹窗外部关闭弹窗 141 | this.managers.uiManager.elements.addModelModal?.addEventListener( 142 | "click", 143 | (e) => { 144 | if (e.target === this.managers.uiManager.elements.addModelModal) { 145 | this.managers.uiManager.hideAddModelModal(); 146 | } 147 | } 148 | ); 149 | 150 | this.managers.uiManager.elements.customProviderModal?.addEventListener( 151 | "click", 152 | (e) => { 153 | if (e.target === this.managers.uiManager.elements.customProviderModal) { 154 | this.managers.uiManager.hideCustomProviderModal(); 155 | } 156 | } 157 | ); 158 | 159 | // 删除服务商对话框相关事件监听 160 | this.managers.uiManager.elements.closeDeleteProviderModal?.addEventListener( 161 | "click", 162 | () => this.managers.uiManager.hideDeleteProviderModal() 163 | ); 164 | 165 | this.managers.uiManager.elements.cancelDeleteProviderButton?.addEventListener( 166 | "click", 167 | () => this.managers.uiManager.hideDeleteProviderModal() 168 | ); 169 | 170 | this.managers.uiManager.elements.confirmDeleteProviderButton?.addEventListener( 171 | "click", 172 | () => this.managers.providerUIManager.handleDeleteProvider() 173 | ); 174 | 175 | this.managers.uiManager.elements.deleteProviderModal?.addEventListener( 176 | "click", 177 | (e) => { 178 | if (e.target === this.managers.uiManager.elements.deleteProviderModal) { 179 | this.managers.uiManager.hideDeleteProviderModal(); 180 | } 181 | } 182 | ); 183 | 184 | // Language selection 185 | this.managers.uiManager.elements.languageSelect.addEventListener( 186 | "change", 187 | (e) => { 188 | this.managers.storageManager.saveLanguage(e.target.value); 189 | this.managers.i18nManager.updateLabels(); 190 | } 191 | ); 192 | 193 | // Model selection 194 | this.managers.uiManager.elements.modelSelect.addEventListener( 195 | "change", 196 | (e) => { 197 | const model = e.target.value; 198 | // 保存模型选择 199 | this.managers.storageManager.saveModel(model).then(() => { 200 | console.log(`✅ 模型已切换为: ${model}`); 201 | }); 202 | } 203 | ); 204 | 205 | // Selection enabled toggle 206 | this.managers.uiManager.elements.selectionEnabled.addEventListener( 207 | "change", 208 | (e) => this.managers.storageManager.saveSelectionEnabled(e.target.checked) 209 | ); 210 | 211 | // Remember window size toggle 212 | this.managers.uiManager.elements.rememberWindowSize.addEventListener( 213 | "change", 214 | (e) => this.managers.storageManager.saveRememberWindowSize(e.target.checked) 215 | ); 216 | 217 | // Pin window toggle 218 | this.managers.uiManager.elements.pinWindow.addEventListener( 219 | "change", 220 | (e) => this.managers.storageManager.savePinWindow(e.target.checked) 221 | ); 222 | 223 | // Shortcut settings 224 | document.getElementById('shortcutSettings').addEventListener( 225 | 'click', 226 | (e) => this.handleShortcutSettings(e) 227 | ); 228 | 229 | // Instructions link 230 | document.getElementById('instructionsLink').addEventListener( 231 | 'click', 232 | (e) => this.handleInstructionsLink(e) 233 | ); 234 | } 235 | 236 | // 处理快捷键设置 237 | handleShortcutSettings(e) { 238 | e.preventDefault(); 239 | chrome.tabs.create({ 240 | url: "chrome://extensions/shortcuts" 241 | }); 242 | } 243 | 244 | // 处理说明链接 245 | async handleInstructionsLink(e) { 246 | e.preventDefault(); 247 | const instructionsUrl = chrome.runtime.getURL('Instructions/Instructions.html'); 248 | chrome.tabs.create({ 249 | url: instructionsUrl 250 | }); 251 | } 252 | } -------------------------------------------------------------------------------- /src/popup/apiKeyManager.js: -------------------------------------------------------------------------------- 1 | export class ApiKeyManager { 2 | constructor(providerManager, uiManager, i18nManager) { 3 | this.providerManager = providerManager; 4 | this.uiManager = uiManager; 5 | this.i18nManager = i18nManager; 6 | this.lastValidatedValue = ''; 7 | } 8 | 9 | // 处理API密钥验证 10 | async handleApiKeyValidation() { 11 | const apiKey = this.uiManager.getApiKeyValue(); 12 | const provider = this.uiManager.elements.providerSelect.value; 13 | 14 | // 如果API key没有变化,不进行验证 15 | if (apiKey === this.lastValidatedValue) { 16 | return; 17 | } 18 | 19 | // 禁用API输入框,防止重复提交 20 | this.uiManager.elements.apiKeyInput.disabled = true; 21 | 22 | // 显示验证中提示和加载动画 23 | this.uiManager.showMessage( 24 | this.i18nManager.getTranslation('validating'), 25 | true 26 | ); 27 | 28 | try { 29 | const settings = { 30 | model: provider === 'custom' ? this.uiManager.getCustomModelName() : this.uiManager.elements.modelSelect.value, 31 | customApiUrl: this.uiManager.getCustomApiUrlValue() 32 | }; 33 | 34 | const isValid = await this.providerManager.validateApiKey(provider, apiKey, settings.model); 35 | 36 | // 恢复API输入框状态 37 | this.uiManager.elements.apiKeyInput.disabled = false; 38 | 39 | if (isValid) { 40 | // 仅在验证成功后保存API密钥 41 | await this.providerManager.saveApiKey(provider, apiKey); 42 | // 更新lastValidatedValue 43 | this.lastValidatedValue = apiKey; 44 | 45 | this.uiManager.showMessage( 46 | this.i18nManager.getTranslation('saveSuccess'), 47 | true 48 | ); 49 | } else { 50 | this.uiManager.showMessage( 51 | this.i18nManager.getTranslation('apiKeyInvalid'), 52 | false 53 | ); 54 | } 55 | } catch (error) { 56 | // 恢复API输入框状态 57 | this.uiManager.elements.apiKeyInput.disabled = false; 58 | 59 | console.error('API验证错误:', error); 60 | this.uiManager.showMessage( 61 | this.i18nManager.getTranslation('fetchError'), 62 | false 63 | ); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/popup/components/dragHandle.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepLifeStudio/DeepSeekAI/b06a7455c0162011bfd092e9f8a6f568abd0bef9/src/popup/components/dragHandle.js -------------------------------------------------------------------------------- /src/popup/i18n.js: -------------------------------------------------------------------------------- 1 | export class I18nManager { 2 | constructor() { 3 | this.translations = { 4 | zh: { 5 | validating: '验证中...', 6 | saveSuccess: '保存成功', 7 | apiKeyInvalid: 'API密钥无效或者检查当前选择的模型是否可用', 8 | noBalance: '余额不足', 9 | noApiKey: '请先设置API密钥', 10 | fetchError: '获取失败', 11 | rememberWindowSize: '保存窗口大小', 12 | customApiUrlSaveSuccess: '自定义API地址已保存', 13 | customApiUrlSaveError: '保存自定义API地址失败', 14 | customApiUrlLabel: '自定义API地址', 15 | customApiUrlPlaceholder: '输入自定义API地址(或使用默认)', 16 | apiKeyEmpty: 'API密钥不能为空', 17 | apiKeyLabel: 'API密钥', 18 | apiKeyPlaceholder: '在此输入API密钥', 19 | balanceText: '余额', 20 | customProviderNameLabel: '服务商名称', 21 | customProviderNamePlaceholder: '输入自定义服务商名称', 22 | customProviderNameExamplePlaceholder: '例如: 我的自定义服务商', 23 | customProviderApiKeyPlaceholder: '请输入API密钥', 24 | customProviderUrlExamplePlaceholder: '例如: https://api.example.com/v1/chat/completions', 25 | customProviderModelNameExamplePlaceholder: '例如: deepseek-chat', 26 | customProviderEmpty: '服务商名称不能为空', 27 | customProviderSaveSuccess: '自定义服务商已保存', 28 | customProviderSaveError: '保存自定义服务商失败', 29 | customProviderApiUrlEmpty: '自定义服务商需要API地址', 30 | customProvider: '自定义服务商', 31 | customModelNameLabel: '模型名称', 32 | customModelNamePlaceholder: '输入模型名称用于API验证', 33 | customModelNameEmpty: '模型名称不能为空', 34 | customModelIdEmpty: '模型ID不能为空', 35 | saveCustomProviderBtnText: '保存', 36 | addModel: '添加模型', 37 | addModelTitle: '添加模型', 38 | modelApiId: '模型API标识', 39 | modelDisplayName: '模型显示名称', 40 | modelApiIdPlaceholder: '输入模型API标识(如 deepseek-chat)', 41 | modelDisplayNamePlaceholder: '输入模型显示名称(如 DeepSeek AI)', 42 | saveModel: '保存', 43 | cancelModel: '取消', 44 | modelSaveSuccess: '模型添加成功', 45 | modelSaveError: '添加模型失败', 46 | modelValidationError: '模型验证失败,该模型可能不存在或不可用', 47 | modelApiIdEmpty: '模型API标识不能为空', 48 | modelDisplayNameEmpty: '模型显示名称不能为空', 49 | modelChanged: '模型已更改为', 50 | processing: '处理中...', 51 | toggle: '显示/隐藏API密钥', 52 | 'header-title': 'DeepSeek AI', 53 | providerLabel: '服务商', 54 | addCustomProvider: '添加服务商', 55 | apiKeyLink: '获取API密钥', 56 | modelLabel: '模型', 57 | selectionEnabledLabel: '快速按钮', 58 | preferredLanguageLabel: '首选语言', 59 | pinWindowLabel: '固定窗口', 60 | shortcutSettingsText: '快捷键设置', 61 | shortcutDescription: '请前往设置快捷键', 62 | instructionsText: '使用说明', 63 | githubText: 'GitHub', 64 | statusText: 'API服务状态', 65 | feedbackText: '反馈', 66 | deleteProvider: '删除服务商', 67 | hideProvider: '隐藏服务商', 68 | deleteProviderConfirm: '确定要删除服务商 {provider}?此操作不可撤销。', 69 | hideProviderConfirm: '确定要隐藏服务商 {provider}?您可以稍后在设置中重新启用它。', 70 | deleteProviderSuccess: '服务商删除成功', 71 | hideProviderSuccess: '服务商已隐藏', 72 | deleteProviderError: '删除服务商失败', 73 | deleteModel: '删除模型', 74 | deleteModelBtnTitle: '删除模型', 75 | deleteProviderBtnTitle: '删除服务商', 76 | confirmDeleteModel: '确定要删除模型 {model}?此操作不可撤销。', 77 | deleteModelSuccess: '模型删除成功', 78 | deleting: '删除中...', 79 | saving: '保存中...' 80 | }, 81 | en: { 82 | validating: 'Validating...', 83 | saveSuccess: 'Saved successfully', 84 | apiKeyInvalid: 'API key is invalid or check if the current selected model is available', 85 | noBalance: 'Insufficient balance', 86 | noApiKey: 'Please set API key first', 87 | fetchError: 'Failed to fetch', 88 | rememberWindowSize: 'Save Window Size', 89 | customApiUrlSaveSuccess: 'Custom API URL saved', 90 | customApiUrlSaveError: 'Failed to save custom API URL', 91 | customApiUrlLabel: 'Custom API URL', 92 | customApiUrlPlaceholder: 'Enter custom API URL (or use default)', 93 | apiKeyEmpty: 'API key cannot be empty', 94 | apiKeyLabel: 'API Key', 95 | apiKeyPlaceholder: 'Enter API Key here', 96 | balanceText: 'Balance', 97 | customProviderNameLabel: 'Provider Name', 98 | customProviderNamePlaceholder: 'Enter custom provider name', 99 | customProviderNameExamplePlaceholder: 'e.g. My Custom Provider', 100 | customProviderApiKeyPlaceholder: 'Please enter API key', 101 | customProviderUrlExamplePlaceholder: 'e.g. https://api.example.com/v1/chat/completions', 102 | customProviderModelNameExamplePlaceholder: 'e.g. deepseek-chat', 103 | customProviderEmpty: 'Provider name cannot be empty', 104 | customProviderSaveSuccess: 'Custom provider saved', 105 | customProviderSaveError: 'Failed to save custom provider', 106 | customProviderApiUrlEmpty: 'Custom provider needs API URL', 107 | customProvider: 'Custom Provider', 108 | customModelNameLabel: 'Model Name', 109 | customModelNamePlaceholder: 'Enter model name for API validation', 110 | customModelNameEmpty: 'Model name cannot be empty', 111 | customModelIdEmpty: 'Model ID cannot be empty', 112 | saveCustomProviderBtnText: 'Save', 113 | addModel: 'Add Model', 114 | addModelTitle: 'Add Model', 115 | modelApiId: 'Model API ID', 116 | modelDisplayName: 'Model Display Name', 117 | modelApiIdPlaceholder: 'Enter model API ID (e.g. deepseek-chat)', 118 | modelDisplayNamePlaceholder: 'Enter model display name (e.g. DeepSeek AI)', 119 | saveModel: 'Save', 120 | cancelModel: 'Cancel', 121 | modelSaveSuccess: 'Model added successfully', 122 | modelSaveError: 'Failed to add model', 123 | modelValidationError: 'Model validation failed, this model may not exist or be unavailable', 124 | modelApiIdEmpty: 'Model API ID cannot be empty', 125 | modelDisplayNameEmpty: 'Model display name cannot be empty', 126 | modelChanged: 'Model changed to', 127 | processing: 'Processing...', 128 | toggle: 'Show/Hide API Key', 129 | 'header-title': 'DeepSeek AI', 130 | providerLabel: 'Service Provider', 131 | addCustomProvider: 'Add Provider', 132 | apiKeyLink: 'Get API Key', 133 | modelLabel: 'Model', 134 | selectionEnabledLabel: 'Quick Button', 135 | preferredLanguageLabel: 'Preferred Language', 136 | pinWindowLabel: 'Pin Window', 137 | shortcutSettingsText: 'Shortcut Settings', 138 | shortcutDescription: 'Please go to set shortcuts', 139 | instructionsText: 'Instructions', 140 | githubText: 'GitHub', 141 | statusText: 'API Service Status', 142 | feedbackText: 'Feedback', 143 | deleteProvider: 'Delete Provider', 144 | hideProvider: 'Hide Provider', 145 | deleteProviderConfirm: 'Are you sure you want to delete {provider}? This action cannot be undone.', 146 | hideProviderConfirm: 'Are you sure you want to hide {provider}? You can re-enable it later in settings.', 147 | deleteProviderSuccess: 'Provider deleted successfully', 148 | hideProviderSuccess: 'Provider hidden successfully', 149 | deleteProviderError: 'Failed to delete provider', 150 | deleteModel: 'Delete Model', 151 | deleteModelBtnTitle: 'Delete Model', 152 | deleteProviderBtnTitle: 'Delete Provider', 153 | confirmDeleteModel: 'Are you sure you want to delete model {model}? This action cannot be undone.', 154 | deleteModelSuccess: 'Model deleted successfully', 155 | deleting: 'Deleting...', 156 | saving: 'Saving...' 157 | } 158 | }; 159 | } 160 | 161 | getCurrentLang() { 162 | return localStorage.getItem('preferredLang') || 'en'; 163 | } 164 | 165 | setCurrentLang(lang) { 166 | localStorage.setItem('preferredLang', lang); 167 | } 168 | 169 | getTranslation(key) { 170 | const currentLang = this.getCurrentLang(); 171 | return this.translations[currentLang][key] || key; 172 | } 173 | 174 | // 更新所有标签的文本内容 175 | updateLabels() { 176 | try { 177 | const currentLang = this.getCurrentLang(); 178 | 179 | // 更新标题 180 | this.updateElementText('header-title', 'header-title'); 181 | 182 | // 更新服务商相关标签 183 | this.updateElementText('providerLabel', 'providerLabel'); 184 | this.updateElementText('customProvider', 'addCustomProvider'); 185 | 186 | // 更新API密钥相关标签 187 | this.updateElementText('apiKeyLabelText', 'apiKeyLabel'); 188 | this.updateElementText('apiKeyLink', 'apiKeyLink'); 189 | 190 | // 更新自定义API URL相关标签 191 | this.updateElementText('customApiUrlLabel', 'customApiUrlLabel'); 192 | 193 | // 更新模型相关标签 194 | this.updateElementText('modelLabel', 'modelLabel'); 195 | 196 | // 更新其他设置标签 197 | this.updateElementText('selectionEnabledLabel', 'selectionEnabledLabel'); 198 | this.updateElementText('preferredLanguageLabel', 'preferredLanguageLabel'); 199 | this.updateElementText('rememberWindowSizeLabel', 'rememberWindowSize'); 200 | this.updateElementText('pinWindowLabel', 'pinWindowLabel'); 201 | 202 | // 更新快捷键设置标签 203 | this.updateElementText('shortcutSettingsText', 'shortcutSettingsText'); 204 | this.updateElementText('shortcutDescription', 'shortcutDescription'); 205 | 206 | // 更新帮助链接标签 207 | this.updateElementText('instructionsText', 'instructionsText'); 208 | this.updateElementText('githubText', 'githubText'); 209 | this.updateElementText('statusText', 'statusText'); 210 | this.updateElementText('feedbackText', 'feedbackText'); 211 | 212 | // 更新输入框占位符 213 | this.updateInputPlaceholder('apiKey', 'apiKeyPlaceholder'); 214 | 215 | // 更新模态窗口中的输入框placeholder 216 | this.updateInputPlaceholder('customProviderNameInput', 'customProviderNameExamplePlaceholder'); 217 | this.updateInputPlaceholder('customProviderApiKey', 'customProviderApiKeyPlaceholder'); 218 | this.updateInputPlaceholder('customApiUrlInput', 'customProviderUrlExamplePlaceholder'); 219 | this.updateInputPlaceholder('customModelIdInput', 'customProviderModelNameExamplePlaceholder'); 220 | this.updateInputPlaceholder('customModelNameInput', 'modelDisplayNamePlaceholder'); 221 | this.updateInputPlaceholder('modelApiId', 'modelApiIdPlaceholder'); 222 | this.updateInputPlaceholder('modelDisplayName', 'modelDisplayNamePlaceholder'); 223 | 224 | // 更新弹窗标签 225 | this.updateElementText('customProviderTitle', 'addCustomProvider'); 226 | this.updateElementText('customProviderNameInputLabel', 'customProviderNameLabel'); 227 | this.updateElementText('customProviderApiKeyLabel', 'apiKeyLabel'); 228 | this.updateElementText('customApiUrlInputLabel', 'customApiUrlLabel'); 229 | this.updateElementText('customModelIdInputLabel', 'modelApiId'); 230 | this.updateElementText('customModelNameInputLabel', 'customModelNameLabel'); 231 | 232 | this.updateElementText('addModelTitle', 'addModelTitle'); 233 | this.updateElementText('modelIdLabel', 'modelApiId'); 234 | this.updateElementText('modelDisplayNameLabel', 'modelDisplayName'); 235 | 236 | this.updateElementText('deleteProviderTitle', 'deleteProvider'); 237 | this.updateElementText('deleteModelTitle', 'deleteModel'); 238 | 239 | // 更新按钮文本 240 | this.updateElementText('saveCustomProviderButton', 'saveCustomProviderBtnText'); 241 | this.updateElementText('cancelCustomProviderButton', 'cancelModel'); 242 | this.updateElementText('saveModelButton', 'saveModel'); 243 | this.updateElementText('cancelModelButton', 'cancelModel'); 244 | this.updateElementText('confirmDeleteProviderButton', 'deleteProvider'); 245 | this.updateElementText('cancelDeleteProviderButton', 'cancelModel'); 246 | this.updateElementText('confirmDeleteModelButton', 'deleteModel'); 247 | this.updateElementText('cancelDeleteModelButton', 'cancelModel'); 248 | } catch (error) { 249 | console.error('更新标签错误:', error); 250 | } 251 | } 252 | 253 | // 更新元素文本 254 | updateElementText(elementId, translationKey) { 255 | const element = document.getElementById(elementId); 256 | if (element) { 257 | element.textContent = this.getTranslation(translationKey); 258 | } 259 | } 260 | 261 | // 更新输入框占位符 262 | updateInputPlaceholder(elementId, translationKey) { 263 | const element = document.getElementById(elementId); 264 | if (element) { 265 | // 保存当前值 266 | const currentValue = element.value; 267 | 268 | // 更新placeholder 269 | element.placeholder = this.getTranslation(translationKey); 270 | 271 | // 确保值不会被清空 272 | if (element.value !== currentValue) { 273 | element.value = currentValue; 274 | } 275 | } 276 | } 277 | } -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | import { ApiKeyManager } from './ApiKeyManager.js'; 2 | import { I18nManager } from './i18n.js'; 3 | import { UiManager } from './uiManager.js'; 4 | import { StorageManager } from './storageManager.js'; 5 | import { ProviderManager } from './ProviderManager.js'; 6 | import { ModelManager } from './ModelManager.js'; 7 | import { ProviderUIManager } from './ProviderUIManager.js'; 8 | import { EventManager } from './EventManager.js'; 9 | 10 | class PopupManager { 11 | constructor() { 12 | // 初始化所有管理器 13 | this.i18nManager = new I18nManager(); 14 | this.uiManager = new UiManager(); 15 | this.storageManager = new StorageManager(); 16 | this.providerManager = new ProviderManager(); 17 | 18 | // 初始化依赖其他管理器的管理器 19 | this.modelManager = new ModelManager( 20 | this.providerManager, 21 | this.storageManager, 22 | this.uiManager, 23 | this.i18nManager 24 | ); 25 | 26 | this.providerUIManager = new ProviderUIManager( 27 | this.providerManager, 28 | this.storageManager, 29 | this.uiManager, 30 | this.i18nManager 31 | ); 32 | 33 | this.apiKeyManager = new ApiKeyManager( 34 | this.providerManager, 35 | this.uiManager, 36 | this.i18nManager 37 | ); 38 | 39 | // 将所有管理器传递给事件管理器 40 | this.eventManager = new EventManager({ 41 | i18nManager: this.i18nManager, 42 | uiManager: this.uiManager, 43 | storageManager: this.storageManager, 44 | providerManager: this.providerManager, 45 | modelManager: this.modelManager, 46 | providerUIManager: this.providerUIManager, 47 | apiKeyManager: this.apiKeyManager 48 | }); 49 | 50 | // 初始化 51 | this.init(); 52 | } 53 | 54 | init() { 55 | if (document.readyState === 'loading') { 56 | document.addEventListener('DOMContentLoaded', () => this.onDOMReady()); 57 | } else { 58 | this.onDOMReady(); 59 | } 60 | } 61 | 62 | onDOMReady() { 63 | // 确保先更新国际化标签 64 | this.i18nManager.updateLabels(); 65 | this.eventManager.initializeEventListeners(); 66 | this.loadInitialState(); 67 | } 68 | 69 | async loadInitialState() { 70 | try { 71 | 72 | // 获取设置 73 | const settings = await this.storageManager.getSettings(); 74 | const currentProvider = settings.provider || 'deepseek'; 75 | 76 | 77 | // 加载所有可见的服务商到下拉菜单 78 | const visibleProviders = await this.providerManager.getAllVisibleProviders(); 79 | 80 | // 清空现有选项 81 | this.uiManager.elements.providerSelect.innerHTML = ''; 82 | 83 | // 添加所有可见的服务商 84 | for (const provider of visibleProviders) { 85 | const option = document.createElement('option'); 86 | option.value = provider.id; 87 | option.textContent = provider.name; 88 | this.uiManager.elements.providerSelect.appendChild(option); 89 | } 90 | 91 | // 添加"添加服务商"选项 92 | const addOption = document.createElement('option'); 93 | addOption.value = 'custom'; 94 | addOption.textContent = this.i18nManager.getTranslation('addCustomProvider'); 95 | addOption.id = 'customProvider'; 96 | this.uiManager.elements.providerSelect.appendChild(addOption); 97 | 98 | // 设置UI元素的初始值 99 | this.uiManager.elements.languageSelect.value = settings.language; 100 | this.uiManager.elements.providerSelect.value = currentProvider; 101 | this.uiManager.elements.selectionEnabled.checked = settings.selectionEnabled; 102 | this.uiManager.elements.rememberWindowSize.checked = settings.rememberWindowSize; 103 | this.uiManager.elements.pinWindow.checked = settings.pinWindow; 104 | 105 | // 只清空API密钥输入框,不清空自定义API地址输入框 106 | this.uiManager.setApiKeyValue(''); 107 | 108 | // 创建自定义服务商下拉菜单 109 | await this.providerUIManager.createCustomProviderDropdown(currentProvider); 110 | 111 | // 更新服务商UI(包括API密钥和自定义API URL的placeholder) 112 | await this.providerUIManager.updateProviderUI(currentProvider); 113 | 114 | // 更新模型选项 115 | await this.modelManager.updateModelOptions(currentProvider); 116 | 117 | // 设置保存的model值 118 | if (settings.model) { 119 | this.uiManager.elements.modelSelect.value = settings.model; 120 | } 121 | 122 | // 再次更新国际化标签,确保所有动态生成的元素也应用了正确的语言 123 | this.i18nManager.updateLabels(); 124 | 125 | } catch (error) { 126 | console.error('初始化错误:', error); 127 | } 128 | } 129 | } 130 | 131 | // 初始化 132 | const popupManager = new PopupManager(); 133 | 134 | // 全局函数,用于更新内容 135 | window.updateContent = () => { 136 | // 只更新标签文本,不触发任何其他操作 137 | if (popupManager && popupManager.i18nManager) { 138 | popupManager.i18nManager.updateLabels(); 139 | } 140 | }; 141 | 142 | // 获取当前语言 143 | const getCurrentLang = () => localStorage.getItem('preferredLang') || 'en'; 144 | 145 | // 设置当前语言 146 | const setCurrentLang = (lang) => localStorage.setItem('preferredLang', lang); 147 | 148 | // 语言切换按钮事件 149 | document.getElementById('language-toggle')?.addEventListener('click', () => { 150 | // 保存当前输入值 151 | const customApiUrlElement = document.getElementById('customApiUrl'); 152 | const apiKeyElement = document.getElementById('apiKey'); 153 | 154 | const currentApiUrl = customApiUrlElement?.value || ''; 155 | const currentApiKey = apiKeyElement?.value || ''; 156 | 157 | // 保存当前选中的服务商和模型 158 | const currentProvider = document.getElementById('provider')?.value || 'deepseek'; 159 | const currentModel = document.getElementById('model')?.value || ''; 160 | 161 | // 切换语言 162 | const currentLang = getCurrentLang(); 163 | const newLang = currentLang === 'zh' ? 'en' : 'zh'; 164 | setCurrentLang(newLang); 165 | 166 | // 更新UI文本 167 | if (popupManager && popupManager.i18nManager) { 168 | popupManager.i18nManager.updateLabels(); 169 | } 170 | 171 | // 恢复输入值 172 | if (customApiUrlElement) { 173 | customApiUrlElement.value = currentApiUrl; 174 | } 175 | if (apiKeyElement) { 176 | apiKeyElement.value = currentApiKey; 177 | } 178 | 179 | // 重新创建自定义服务商下拉菜单 180 | if (popupManager && popupManager.providerUIManager) { 181 | popupManager.providerUIManager.createCustomProviderDropdown(currentProvider); 182 | } 183 | 184 | // 重新创建模型下拉菜单 185 | if (popupManager && popupManager.modelManager) { 186 | popupManager.modelManager.updateModelOptions(currentProvider).then(() => { 187 | // 恢复之前选中的模型 188 | const modelSelect = document.getElementById('model'); 189 | if (modelSelect && currentModel) { 190 | modelSelect.value = currentModel; 191 | } 192 | 193 | // 确保模态窗口中的文本也被更新 194 | if (popupManager && popupManager.i18nManager) { 195 | popupManager.i18nManager.updateLabels(); 196 | } 197 | }); 198 | } 199 | }); 200 | -------------------------------------------------------------------------------- /src/popup/storageManager.js: -------------------------------------------------------------------------------- 1 | export class StorageManager { 2 | constructor() { 3 | // 缓存最后保存的设置 4 | this.cachedSettings = null; 5 | } 6 | 7 | // 获取缓存的设置(用于不需要等待异步操作的场景) 8 | getCachedSettings() { 9 | return this.cachedSettings; 10 | } 11 | 12 | async getSettings() { 13 | return new Promise((resolve) => { 14 | chrome.storage.sync.get( 15 | ["deepseekApiKey", "siliconflowApiKey", "openrouterApiKey","volcengineApiKey" ,"tencentcloudApiKey", "iflytekstarApiKey","baiducloudApiKey","aliyunApiKey", "aihubmixApiKey", 16 | "deepseekCustomApiUrl", "siliconflowCustomApiUrl", "openrouterCustomApiUrl", "volcengineCustomApiUrl", "tencentcloudCustomApiUrl", "iflytekstarCustomApiUrl", "baiducloudCustomApiUrl", "aliyunCustomApiUrl", "aihubmixCustomApiUrl", 17 | "language", "model", "provider", "selectionEnabled", "rememberWindowSize", "pinWindow", "customModels"], 18 | (data) => { 19 | // 更新缓存 20 | this.cachedSettings = { 21 | deepseekApiKey: data.deepseekApiKey || '', 22 | siliconflowApiKey: data.siliconflowApiKey || '', 23 | openrouterApiKey: data.openrouterApiKey || '', 24 | volcengineApiKey: data.volcengineApiKey || '', 25 | tencentcloudApiKey: data.tencentcloudApiKey || '', 26 | iflytekstarApiKey: data.iflytekstarApiKey || '', 27 | baiducloudApiKey: data.baiducloudApiKey || '', 28 | aliyunApiKey: data.aliyunApiKey || '', 29 | aihubmixApiKey: data.aihubmixApiKey || '', 30 | deepseekCustomApiUrl: data.deepseekCustomApiUrl || '', 31 | siliconflowCustomApiUrl: data.siliconflowCustomApiUrl || '', 32 | openrouterCustomApiUrl: data.openrouterCustomApiUrl || '', 33 | volcengineCustomApiUrl: data.volcengineCustomApiUrl || '', 34 | tencentcloudCustomApiUrl: data.tencentcloudCustomApiUrl || '', 35 | iflytekstarCustomApiUrl: data.iflytekstarCustomApiUrl || '', 36 | baiducloudCustomApiUrl: data.baiducloudCustomApiUrl || '', 37 | aliyunCustomApiUrl: data.aliyunCustomApiUrl || '', 38 | aihubmixCustomApiUrl: data.aihubmixCustomApiUrl || '', 39 | language: data.language || 'en', 40 | model: data.model || 'deepseek-chat', 41 | provider: data.provider || 'deepseek', 42 | selectionEnabled: typeof data.selectionEnabled === 'undefined' ? true : data.selectionEnabled, 43 | rememberWindowSize: typeof data.rememberWindowSize === 'undefined' ? false : data.rememberWindowSize, 44 | pinWindow: typeof data.pinWindow === 'undefined' ? false : data.pinWindow, 45 | customModels: data.customModels || {} 46 | }; 47 | resolve(this.cachedSettings); 48 | } 49 | ); 50 | }); 51 | } 52 | 53 | async saveApiKey(provider, apiKey) { 54 | const key = `${provider}ApiKey`; 55 | return new Promise((resolve) => { 56 | chrome.storage.sync.set({ [key]: apiKey }, () => { 57 | // 更新缓存 58 | if (this.cachedSettings) { 59 | this.cachedSettings[key] = apiKey; 60 | } 61 | resolve(); 62 | }); 63 | }); 64 | } 65 | 66 | async saveLanguage(language) { 67 | return new Promise((resolve) => { 68 | chrome.storage.sync.set({ language }, () => { 69 | // 更新缓存 70 | if (this.cachedSettings) { 71 | this.cachedSettings.language = language; 72 | } 73 | resolve(); 74 | }); 75 | }); 76 | } 77 | 78 | async saveModel(model) { 79 | return new Promise((resolve) => { 80 | chrome.storage.sync.set({ model }, () => { 81 | // 更新缓存 82 | if (this.cachedSettings) { 83 | this.cachedSettings.model = model; 84 | } 85 | resolve(); 86 | }); 87 | }); 88 | } 89 | 90 | async saveProvider(provider) { 91 | return new Promise((resolve) => { 92 | chrome.storage.sync.set({ provider }, () => { 93 | // 更新缓存 94 | if (this.cachedSettings) { 95 | this.cachedSettings.provider = provider; 96 | } 97 | resolve(); 98 | }); 99 | }); 100 | } 101 | 102 | async saveSelectionEnabled(enabled) { 103 | return new Promise((resolve) => { 104 | chrome.storage.sync.set({ selectionEnabled: enabled }, () => { 105 | // 更新缓存 106 | if (this.cachedSettings) { 107 | this.cachedSettings.selectionEnabled = enabled; 108 | } 109 | resolve(); 110 | }); 111 | }); 112 | } 113 | 114 | async saveRememberWindowSize(enabled) { 115 | return new Promise((resolve) => { 116 | chrome.storage.sync.set({ rememberWindowSize: enabled }, () => { 117 | // 更新缓存 118 | if (this.cachedSettings) { 119 | this.cachedSettings.rememberWindowSize = enabled; 120 | } 121 | resolve(); 122 | }); 123 | }); 124 | } 125 | 126 | async savePinWindow(enabled) { 127 | return new Promise((resolve) => { 128 | chrome.storage.sync.set({ pinWindow: enabled }, () => { 129 | // 更新缓存 130 | if (this.cachedSettings) { 131 | this.cachedSettings.pinWindow = enabled; 132 | } 133 | resolve(); 134 | }); 135 | }); 136 | } 137 | 138 | async saveCustomApiUrl(provider, customApiUrl) { 139 | const key = `${provider}CustomApiUrl`; 140 | return new Promise((resolve) => { 141 | chrome.storage.sync.set({ [key]: customApiUrl }, () => { 142 | // 更新缓存 143 | if (this.cachedSettings) { 144 | this.cachedSettings[key] = customApiUrl; 145 | } 146 | resolve(); 147 | }); 148 | }); 149 | } 150 | 151 | // 获取指定服务商的自定义模型 152 | async getCustomModels(provider) { 153 | return new Promise((resolve) => { 154 | chrome.storage.sync.get(['customModels'], (data) => { 155 | const customModels = data.customModels || {}; 156 | resolve(customModels[provider] || []); 157 | }); 158 | }); 159 | } 160 | 161 | // 保存自定义模型 162 | async saveCustomModel(provider, modelId, displayName) { 163 | return new Promise(async (resolve, reject) => { 164 | try { 165 | // 获取当前保存的自定义模型 166 | const settings = await this.getSettings(); 167 | const customModels = settings.customModels || {}; 168 | const providerModels = customModels[provider] || []; 169 | 170 | // 检查模型ID是否已存在 171 | const existingModelIndex = providerModels.findIndex(model => model.value === modelId); 172 | if (existingModelIndex >= 0) { 173 | // 如果已存在,更新显示名称 174 | providerModels[existingModelIndex].label = displayName; 175 | } else { 176 | // 如果不存在,添加新模型 177 | providerModels.push({ 178 | value: modelId, 179 | label: displayName 180 | }); 181 | } 182 | 183 | // 更新保存 184 | customModels[provider] = providerModels; 185 | 186 | chrome.storage.sync.set({ customModels }, () => { 187 | // 更新缓存 188 | if (this.cachedSettings) { 189 | this.cachedSettings.customModels = customModels; 190 | } 191 | resolve(); 192 | }); 193 | } catch (error) { 194 | reject(error); 195 | } 196 | }); 197 | } 198 | 199 | // 删除自定义模型 200 | async deleteCustomModel(provider, modelId) { 201 | return new Promise(async (resolve, reject) => { 202 | try { 203 | // 获取当前保存的自定义模型 204 | const settings = await this.getSettings(); 205 | const customModels = settings.customModels || {}; 206 | const providerModels = customModels[provider] || []; 207 | 208 | // 过滤掉要删除的模型 209 | const updatedModels = providerModels.filter(model => model.value !== modelId); 210 | 211 | // 更新保存 212 | customModels[provider] = updatedModels; 213 | 214 | // 直接更新 storage 215 | await this.updateStorage('customModels', customModels); 216 | 217 | // 为了确保完全删除,也更新独立的provider存储 218 | const storageKey = `customModels_${provider}`; 219 | try { 220 | // 从chrome.storage.sync中获取数据 221 | const data = await this.getFromStorage(storageKey); 222 | if (data) { 223 | const providerCustomModels = data.filter(model => model.value !== modelId); 224 | // 更新storage 225 | await this.updateStorage(storageKey, providerCustomModels); 226 | } 227 | } catch (error) { 228 | console.error('更新provider特定存储时出错:', error); 229 | // 继续执行,不阻止主要删除操作 230 | } 231 | 232 | resolve(); 233 | } catch (error) { 234 | reject(error); 235 | } 236 | }); 237 | } 238 | 239 | // 辅助方法:从storage中获取数据 240 | async getFromStorage(key) { 241 | return new Promise((resolve, reject) => { 242 | chrome.storage.sync.get(key, (result) => { 243 | if (chrome.runtime.lastError) { 244 | reject(chrome.runtime.lastError); 245 | } else { 246 | resolve(result[key]); 247 | } 248 | }); 249 | }); 250 | } 251 | 252 | // 辅助方法:更新storage中的数据 253 | async updateStorage(key, value) { 254 | return new Promise((resolve, reject) => { 255 | chrome.storage.sync.set({ [key]: value }, () => { 256 | if (chrome.runtime.lastError) { 257 | reject(chrome.runtime.lastError); 258 | } else { 259 | // 更新缓存 260 | if (this.cachedSettings && key in this.cachedSettings) { 261 | this.cachedSettings[key] = value; 262 | } 263 | resolve(); 264 | } 265 | }); 266 | }); 267 | } 268 | 269 | // 保存指定提供商的所有自定义模型 270 | async saveCustomModels(provider, models) { 271 | return new Promise(async (resolve, reject) => { 272 | try { 273 | // 获取当前保存的所有自定义模型 274 | const settings = await this.getSettings(); 275 | const customModels = settings.customModels || {}; 276 | 277 | // 更新指定提供商的模型列表 278 | customModels[provider] = models; 279 | 280 | // 保存到chrome.storage.sync 281 | await this.updateStorage('customModels', customModels); 282 | 283 | // 同时保存到provider特定的存储中,确保数据一致性 284 | const storageKey = `customModels_${provider}`; 285 | localStorage.setItem(storageKey, JSON.stringify(models)); 286 | 287 | resolve(); 288 | } catch (error) { 289 | console.error('保存自定义模型列表失败:', error); 290 | reject(error); 291 | } 292 | }); 293 | } 294 | 295 | // 保存默认模型列表 296 | async saveDefaultModels(provider, models) { 297 | return new Promise(async (resolve, reject) => { 298 | try { 299 | const storageKey = `defaultModels_${provider}`; 300 | localStorage.setItem(storageKey, JSON.stringify(models)); 301 | resolve(); 302 | } catch (error) { 303 | console.error('保存默认模型列表失败:', error); 304 | reject(error); 305 | } 306 | }); 307 | } 308 | 309 | // 获取指定服务商的自定义模型(从localStorage读取) 310 | async getCustomModelsFromLocalStorage(provider) { 311 | try { 312 | const storageKey = `customModels_${provider}`; 313 | const customModelsJson = localStorage.getItem(storageKey); 314 | if (customModelsJson) { 315 | return JSON.parse(customModelsJson); 316 | } 317 | return []; 318 | } catch (e) { 319 | console.error('从localStorage获取自定义模型失败:', e); 320 | return []; 321 | } 322 | } 323 | 324 | // 获取指定服务商的默认模型(从localStorage读取) 325 | async getDefaultModelsFromLocalStorage(provider) { 326 | try { 327 | const storageKey = `defaultModels_${provider}`; 328 | const defaultModelsJson = localStorage.getItem(storageKey); 329 | if (defaultModelsJson !== null) { 330 | return JSON.parse(defaultModelsJson); 331 | } 332 | return null; // 返回null而不是空数组,表示没有设置 333 | } catch (e) { 334 | console.error('从localStorage获取默认模型失败:', e); 335 | return null; 336 | } 337 | } 338 | 339 | // 保存插件设置 340 | async saveSettings(settings) { 341 | return new Promise((resolve) => { 342 | chrome.storage.sync.set(settings, () => { 343 | // 更新缓存 344 | this.cachedSettings = {...this.cachedSettings, ...settings}; 345 | resolve(); 346 | }); 347 | }); 348 | } 349 | } -------------------------------------------------------------------------------- /test/selection-test.html: -------------------------------------------------------------------------------- 1 |
这是测试文本,请选择我然后看看效果。This is test text, please select me and see the effect.
2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: "./src/content/content.js", 7 | output: { 8 | filename: "content.js", 9 | path: path.resolve(__dirname, "dist"), 10 | clean: true 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['@babel/preset-env'] 20 | } 21 | }, 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: ["style-loader", "css-loader"], 27 | } 28 | ], 29 | }, 30 | plugins: [ 31 | new CopyPlugin({ 32 | patterns: [ 33 | { 34 | from: "./src/manifest.json", 35 | to: "manifest.json", 36 | transform(content) { 37 | return Buffer.from(JSON.stringify(JSON.parse(content), null, 2), 'utf-8') 38 | } 39 | }, 40 | { from: "./src/icons", to: "icons" }, 41 | { from: "./src/content/styles/style.css", to: "style.css" }, 42 | { from: "./src/popup", to: "popup" }, 43 | { from: "./src/background.js", to: "background.js" }, 44 | { from: "./src/Instructions/Instructions.html", to: "Instructions/Instructions.html" }, 45 | { from: "./src/Instructions/instructions.js", to: "Instructions/instructions.js" } 46 | ], 47 | }), 48 | ], 49 | optimization: { 50 | minimize: true, 51 | minimizer: [ 52 | new TerserPlugin({ 53 | terserOptions: { 54 | format: { 55 | comments: false, 56 | ascii_only: true 57 | } 58 | }, 59 | extractComments: false, 60 | }), 61 | ], 62 | }, 63 | resolve: { 64 | extensions: [".js"], 65 | }, 66 | mode: "production", 67 | }; 68 | -------------------------------------------------------------------------------- /~/.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "neon": { 4 | "command": "npx", 5 | "args": ["-y", "@neondatabase/mcp-server-neon", "start", ""] 6 | }, 7 | "browser-tools": { 8 | "command": "npx", 9 | "args": ["-y", "@agentdeskai/browser-tools-mcp@1.2.0"] 10 | } 11 | } 12 | } --------------------------------------------------------------------------------