├── chrome扩展20250611.zip ├── README.md └── tampermonkey脚本20250614.js /chrome扩展20250611.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilikeeu/spassword/HEAD/chrome扩展20250611.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ----- 2 | - 油猴脚本需要修改www.deno.dev为你部署的地址。有3处替换 3 | - 注意一定要先添加OAUTH_ID,指定授权账号ID。你使用的那个第三方授权就填写那个账号ID 4 | - 使用chrome扩展,在系统管理页面有课能要多刷新几次令牌才会显示 5 | 6 | - 注意修改tampermonkey代码.js里面的密码管理系统地址 7 | - **更新记录:20256.11 8 | - **修复误操作输错密码,历史密码恢复 9 | - **增加chrome扩展 10 | - **已安装的如果报错,请尝试删除最近的一条KV数据 11 | - **再次更新 12 | - **增加历史密码记录删除按钮 13 | - **增加系统分类添加,账号编辑功能优化 14 | - **修改tampermonkey脚本在检测到登录框的时候才会显示在右下角 15 | - **修改控制脚本和扩展访问频率 16 | - 新增加D1数据库版本 https://github.com/ilikeeu/Spassword-d1 17 | - 20250614增加deno KV版本https://github.com/ilikeeu/spassword-deno 18 | # 🔐 密码管理器 Pro - 完整说明文档 19 | 20 | ## 🎯 项目概述 21 | 22 | **密码管理器 Pro** 是一个基于 Cloudflare Workers 的现代化密码管理解决方案,提供安全的密码存储、智能自动填充、云备份同步等功能。 23 | 24 | ### 🌟 核心优势 25 | 26 | - **🔒 端到端加密**:所有密码数据采用 AES-GCM 加密 27 | - **☁️ 云原生架构**:基于 Cloudflare Workers + KV 存储 28 | - **🤖 智能自动填充**:配套 Tampermonkey 扩展实现自动填充 29 | - **📱 响应式设计**:完美适配桌面和移动设备 30 | - **🔄 WebDAV 备份**:支持多种云存储服务 31 | - **👥 多用户支持**:OAuth 认证 + 用户隔离 32 | 33 | ## ✨ 功能特性 34 | 35 | #### 🔐 密码管理 36 | 37 | - ✅ 密码增删改查 38 | - ✅ 分类管理 39 | - ✅ 批量导入导出 40 | - ✅ 密码强度生成器 41 | - ✅ 重复检测 42 | - ✅ 分页浏览(50条/页) 43 | 44 | #### 🤖 智能填充 45 | 46 | - ✅ 自动检测登录表单 47 | - ✅ 智能匹配算法(精确/子域/站名) 48 | - ✅ 多账户选择 49 | - ✅ 密码变更检测 50 | - ✅ 一键填充 51 | 52 | #### ☁️ 云备份 53 | 54 | - ✅ WebDAV 加密备份 55 | - ✅ 支持 TeraCloud、坚果云、NextCloud 56 | - ✅ 自动去重恢复 57 | - ✅ 备份文件管理 58 | 59 | #### 🔒 安全特性 60 | 61 | - ✅ OAuth 第三方认证 62 | - ✅ 用户权限控制 63 | - ✅ 会话管理 64 | - ✅ 数据加密存储 65 | 66 | ## 🚀 部署指南 67 | 68 | ### 1\. 准备工作 69 | 70 | #### 1.1 创建 OAuth 应用 71 | 72 | 选择一个 OAuth 提供商(如 GitHub、GitLab、Google),创建 OAuth 应用: 73 | 74 | **GitHub 示例:** 75 | 76 | 1. 访问 [GitHub Developer Settings](https://github.com/settings/developers) 77 | 2. 点击 "New OAuth App" 78 | 3. 填写应用信息: 79 | - **Application name**: `密码管理器 Pro` 80 | - **Homepage URL**: `https://your-domain.pages.dev` 81 | - **Authorization callback URL**: `https://your-domain.pages.dev/api/oauth/callback` 82 | 4. 获取 `Client ID` 和 `Client Secret` 83 | 84 | #### 1.2 获取用户 ID(可选) 85 | 86 | 如果需要单用户访问控制: 87 | 88 | 1. 完成 OAuth 应用创建后 89 | 2. 访问提供商的 API 获取用户 ID 90 | - **GitHub 示例**:`https://api.github.com/user` 91 | 92 | ### 2\. Cloudflare Workers 部署 93 | 94 | #### 2.1 创建 Workers 项目 95 | 96 | ```bash 97 | # 安装 Wrangler CLI 98 | npm install -g wrangler 99 | 100 | # 登录 Cloudflare 101 | wrangler auth login 102 | 103 | # 创建项目 104 | wrangler init password-manager 105 | cd password-manager 106 | ``` 107 | 108 | #### 2.2 配置 `wrangler.toml` 109 | 110 | ```toml 111 | name = "password-manager" 112 | main = "src/worker.js" 113 | compatibility_date = "2024-01-01" 114 | 115 | [[kv_namespaces]] 116 | binding = "PASSWORD_KV" 117 | id = "your-kv-namespace-id" 118 | preview_id = "your-preview-kv-namespace-id" 119 | 120 | [vars] 121 | OAUTH_BASE_URL = "https://github.com" 122 | OAUTH_REDIRECT_URI = "https://your-domain.pages.dev/api/oauth/callback" 123 | 124 | [env.production.vars] 125 | OAUTH_CLIENT_ID = "your-oauth-client-id" 126 | 127 | # 生产环境的 secrets 128 | [env.production.secrets] 129 | OAUTH_CLIENT_SECRET = "your-oauth-client-secret" 130 | OAUTH_ID = "your-user-id" # 可选:单用户访问控制 131 | ``` 132 | 133 | #### 2.3 创建 KV 命名空间 134 | 135 | ```bash 136 | # 创建生产环境的 KV 命名空间 137 | wrangler kv:namespace create "PASSWORD_KV" 138 | 139 | # 创建预览环境的 KV 命名空间 140 | wrangler kv:namespace create "PASSWORD_KV" --preview 141 | ``` 142 | 143 | #### 2.4 部署代码 144 | 145 | 1. 复制完整的 `_worker.js` 代码到 `src/worker.js` 146 | 2. 设置 Secret 环境变量: 147 | ```bash 148 | wrangler secret put OAUTH_CLIENT_SECRET 149 | wrangler secret put OAUTH_ID # 可选 150 | ``` 151 | 3. 部署: 152 | ```bash 153 | wrangler deploy 154 | ``` 155 | 156 | ### 3\. 自定义域名(推荐) 157 | 158 | 1. **添加域名路由** 159 | ```bash 160 | wrangler route add "your-domain.com/*" password-manager 161 | ``` 162 | 2. **配置 DNS** 163 | 在 Cloudflare DNS 设置中添加 CNAME 记录: 164 | `CNAME` | `@` | `your-worker.your-subdomain.workers.dev` 165 | 166 | ## ⚙️ 配置说明 167 | 168 | ### 环境变量 169 | 170 | | 变量名 | 类型 | 必需 | 说明 | 171 | | --------------------- | -------- | :--: | ------------------------------ | 172 | | `OAUTH_CLIENT_ID` | Secret | ✅ | OAuth 应用 Client ID | 173 | | `OAUTH_CLIENT_SECRET` | Secret | ✅ | OAuth 应用 Client Secret | 174 | | `OAUTH_BASE_URL` | Variable | ✅ | OAuth 提供商基础 URL | 175 | | `OAUTH_REDIRECT_URI` | Variable | ✅ | OAuth 回调地址 | 176 | | `OAUTH_ID` | Secret | ❌ | 单用户访问控制的用户 ID | 177 | 178 | ### OAuth 提供商配置 179 | 180 | - **GitHub**: `OAUTH_BASE_URL = "https://github.com"` 181 | - **GitLab**: `OAUTH_BASE_URL = "https://gitlab.com"` 182 | - **Google**: `OAUTH_BASE_URL = "https://accounts.google.com"` 183 | 184 | ### KV 存储结构 185 | 186 | - **用户会话**: `session_{token}` = `{用户信息}` 187 | - **密码数据**: `password_{userId}_{passwordId}` = `{加密密码数据}` 188 | - **分类数据**: `categories_{userId}` = `[分类列表]` 189 | - **WebDAV 配置**: `webdav_config_{userId}` = `{加密WebDAV配置}` 190 | - **OAuth 状态**: `oauth_state_{state}` = `"valid"` 191 | 192 | ## 🔧 Tampermonkey扩展 193 | 194 | ### 安装步骤 195 | 196 | 1. **安装 Tampermonkey** 197 | - Chrome: [Chrome Web Store](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) 198 | - Firefox: [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/tampermonkey/) 199 | - Edge: [Microsoft Store](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) 200 | 2. **添加脚本** 201 | - 点击 Tampermonkey 图标 → "添加新脚本" 202 | - 复制完整的扩展代码并粘贴 203 | - 保存脚本 204 | 3. **配置扩展** 205 | - 访问密码管理器网站并登录 206 | - 扩展会自动获取登录令牌 207 | - 或手动设置令牌:右键 → Tampermonkey → 设置令牌 208 | 209 | ### 扩展功能 210 | 211 | - **🔍 自动检测** 212 | - 自动检测页面登录表单 213 | - 智能匹配已保存的账户 214 | - 显示匹配统计(精确/子域/站名) 215 | - **⚡ 快速填充** 216 | - 单账户:显示快速填充按钮 217 | - 多账户:显示账户选择列表 218 | - 一键填充用户名和密码 219 | - **💾 自动保存** 220 | - 检测表单提交 221 | - 自动保存新账户 222 | - 检测密码变更并提示更新 223 | - **🎯 智能匹配** 224 | - **精确匹配**:域名完全相同(优先级最高) 225 | - **子域匹配**:子域名匹配 226 | - **站名匹配**:网站名称包含关键词 227 | 228 | ### 使用技巧 229 | 230 | - **快捷键** 231 | - `Ctrl+K`:快速搜索 232 | - `Esc`:关闭弹窗 233 | - **菜单命令** 234 | - 打开密码管理器 235 | - 重新检测表单 236 | - 设置令牌 237 | - 退出登录 238 | - 调试信息 239 | 240 | ## 📖 API文档 241 | 242 | ### 认证相关 243 | 244 | - **登录授权**: `GET /api/oauth/login` 245 | - 返回 OAuth 授权链接 246 | - **OAuth 回调**: `GET /api/oauth/callback?code={code}&state={state}` 247 | - 处理 OAuth 回调并创建会话 248 | - **验证登录状态**: `GET /api/auth/verify` (`Authorization: Bearer {token}`) 249 | - **登出**: `POST /api/auth/logout` (`Authorization: Bearer {token}`) 250 | 251 | ### 密码管理 252 | 253 | - **获取密码列表**: `GET /api/passwords?page=1&limit=50&search={query}&category={category}` 254 | - **添加密码**: `POST /api/passwords` 255 | ```json 256 | { 257 | "siteName": "GitHub", 258 | "username": "user@example.com", 259 | "password": "password123", 260 | "url": "https://github.com", 261 | "category": "开发工具", 262 | "notes": "备注信息" 263 | } 264 | ``` 265 | - **更新密码**: `PUT /api/passwords/{id}` 266 | ```json 267 | { 268 | "siteName": "GitHub", 269 | "password": "newpassword123" 270 | } 271 | ``` 272 | - **删除密码**: `DELETE /api/passwords/{id}` 273 | - **获取明文密码**: `GET /api/passwords/{id}/reveal` 274 | 275 | ### 自动填充 276 | 277 | - **检测登录**: `POST /api/detect-login` 278 | ```json 279 | { 280 | "url": "https://github.com/login", 281 | "username": "user@example.com", 282 | "password": "password123" 283 | } 284 | ``` 285 | - **自动填充匹配**: `POST /api/auto-fill` 286 | ```json 287 | { 288 | "url": "https://github.com/login" 289 | } 290 | ``` 291 | 292 | ### WebDAV 备份 293 | 294 | - **保存配置**: `POST /api/webdav/config` 295 | - **测试连接**: `POST /api/webdav/test` 296 | - **创建备份**: `POST /api/webdav/backup` 297 | - **恢复备份**: `POST /api/webdav/restore` 298 | 299 | ## 📚 使用教程 300 | 301 | 1. **首次使用** 302 | 1. **访问网站**:打开您部署的密码管理器网址 303 | 2. **开始使用**:点击 "开始使用 OAuth 登录" 304 | 3. **完成认证**:跳转到 OAuth 提供商,授权应用访问 305 | 4. **安装扩展**:安装 Tampermonkey 扩展并添加脚本,扩展会自动同步登录状态 306 | 2. **密码管理** 307 | - **添加密码**: 点击 "添加密码" 标签页,填写信息并保存。可使用密码生成器创建强密码。 308 | - **管理密码**: 在 "密码管理" 标签页,可通过搜索、分类筛选和分页浏览所有密码。 309 | - **密码操作**: 310 | - 👁️ **查看**:显示明文密码 311 | - 📋 **复制**:复制密码到剪贴板 312 | - ✏️ **编辑**:修改密码信息 313 | - 🗑️ **删除**:删除密码(不可恢复) 314 | 3. **WebDAV 备份** 315 | 1. **配置**: 在 "云备份" 标签页填写您的 WebDAV 服务信息,测试并保存。 316 | 2. **创建备份**: 设置一个备份密码,点击 "创建加密备份"。 317 | 3. **恢复备份**: 列出云端文件,选择备份文件,输入备份密码后即可恢复。系统会自动去重。 318 | 4. **自动填充** 319 | - 访问任意登录页面,扩展会自动检测。 320 | - 点击右下角浮动按钮选择账户进行填充。 321 | - 登录成功后,新账户会自动保存,密码变更会自动提示。 322 | 323 | ## 🔒 安全说明 324 | 325 | ### 加密机制 326 | 327 | - **密码加密**: 328 | - 算法:AES-GCM 256位 329 | - 密钥:基于用户ID生成 330 | - 初始化向量:每次加密随机生成 331 | - 存储:Base64编码存储 332 | - **备份加密**: 333 | - 算法:AES-GCM 256位 334 | - 密钥:基于用户设置的备份密码 335 | - 双重加密:密码先用用户密钥加密,再用备份密码加密 336 | 337 | ### 安全最佳实践 338 | 339 | - **部署安全**: 强制 HTTPS,绑定域名,使用 Secrets 管理敏感信息,设置 `OAUTH_ID` 限制访问。 340 | - **使用安全**: OAuth 账户使用强密码,定期备份,不分享登录令牌,及时登出。 341 | - **风险提示**: 注意浏览器、网络环境和设备安全,妥善保管备份密码。 342 | 343 | ## 🛠️ 故障排除 344 | 345 | ### 常见问题 346 | 347 | 1. **登录失败** 348 | - **症状**:点击登录后跳转失败或提示错误。 349 | - **解决方案**:检查 `wrangler.toml` 中的 OAuth 配置与回调地址是否正确;使用 `wrangler tail` 查看实时日志。 350 | 2. **扩展无法填充** 351 | - **症状**:扩展检测到账户但填充失败。 352 | - **解决方案**:打开浏览器开发者工具查看控制台错误;确认页面字段是否可见;尝试手动刷新。 353 | 3. **WebDAV 连接失败** 354 | - **症状**:测试连接时提示失败。 355 | - **解决方案**:确认 WebDAV 地址、用户名、密码无误;检查网络连接。 356 | 357 | ### 调试工具 358 | 359 | - **扩展调试**: Tampermonkey 菜单 → `调试信息`,查看控制台输出。 360 | - **API 调试**: 361 | ```bash 362 | # 查看 Workers 日志 363 | wrangler tail 364 | 365 | # 测试 API 端点 366 | curl -H "Authorization: Bearer {token}" https://your-domain.pages.dev/api/passwords 367 | ``` 368 | - **KV 存储调试**: 369 | ```bash 370 | # 列出 KV 键 371 | wrangler kv:key list --binding=PASSWORD_KV 372 | 373 | # 查看特定键值 374 | wrangler kv:key get "session_xxx" --binding=PASSWORD_KV 375 | ``` 376 | 377 | ## 📝 更新日志 378 | 379 | ### `v1.7.0` 380 | 381 | - **新增** 382 | - ✅ 分页功能(50条/页) 383 | - ✅ 用户授权控制(`OAUTH_ID`) 384 | - ✅ 智能密码变更检测 385 | - ✅ 改进的重复检查逻辑 386 | - ✅ WebDAV 测试连接功能 387 | - **修复** 388 | - 🐛 Tampermonkey 全局函数作用域问题 389 | - 🐛 密码填充失败问题 390 | - 🐛 分页导航显示问题 391 | - **优化** 392 | - 🔧 更好的错误处理和日志 393 | - 🔧 响应式界面优化 394 | - 🔧 API 性能提升 395 | 396 | ### `v1.6.0` 397 | 398 | - **新增** 399 | - ✅ WebDAV 云备份支持 400 | - ✅ 加密导入导出 401 | - ✅ 自动去重恢复 402 | 403 | ### `v1.5.0` 404 | 405 | - **新增** 406 | - ✅ Tampermonkey 扩展 407 | - ✅ 自动登录检测 408 | - ✅ 智能密码填充 409 | 410 | ### `v1.0.0` 411 | 412 | - **初始发布** 413 | - ✅ 基础密码管理 414 | - ✅ OAuth 认证 415 | 416 | ## 📞 支持与反馈 417 | 418 | - **获取帮助**: 查看本文档,或在项目的 `Issues` 和 `Discussions` 区提问。 419 | - **贡献代码**: 欢迎 Fork 项目仓库,创建功能分支,并发起 Pull Request。 420 | - **许可证**: 本项目采用 MIT 许可证。 421 | 422 | ----- 423 | 424 | **🔐 密码管理器 Pro - 让密码管理变得简单、安全、智能!** 425 | -------------------------------------------------------------------------------- /tampermonkey脚本20250614.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 智能密码管理助手 Pro - Material-UI完全修复版 3 | // @namespace https://www.deno.dev/ 4 | // @version 3.1.5 5 | // @description 自动检测和填充密码,支持多账户切换、密码变更检测和历史记录管理。完全修复Material-UI受控组件填充问题。 6 | // @author Password Manager Pro 7 | // @match *://*/* 8 | // @grant GM_xmlhttpRequest 9 | // @grant GM_setValue 10 | // @grant GM_getValue 11 | // @grant GM_addStyle 12 | // @grant GM_registerMenuCommand 13 | // @grant GM_setClipboard 14 | // @run-at document-end 15 | // @icon data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACdUlEQVR4nF2TPWtcVxCGnzn3Siuhj0iOVMgLLiT8A+yg1HaXbZIqaVIIUijgLsHg36DG6QRhsQuXSWEMwYka24XTCNJZARGIYuGNEkVeRbY+9u45Z14X98oYD0wzzDsf58xjAAIzEMB/sAysDuHaANoJcOhleHIG3WXYfFdjAgP4DcpFWANuGLSOgQEQ62QCcArVANafwa2vIQkwQQGE/+HBjFmnV4tyMgtJsigRQ1ACT1IxLbEPv3wAn/4JHgxyH9ZmoPO3NMySXCqSu2UJD4Hsbsm9yJL2YTgNn/wLa19AtkO4arA5BOL4eMjj41YdHWGLiwz29zk7OqJot9HkJK+2t4kgmXmUrIJlXsLdQVlqF9LhzZvK/b5SrydJqp4/1+unT3VuBw8faqPV0k9macNMP8Ld4HD9tH7pwNgYYXaWwzt3eLG6yuilS7QuX+b3lRUO7t/nw06HqStXqKQwCIEM14PDQlUXMJdA4mB9nb1ul9zvc7q1xV/37tHrdpE7IxcvksGiGRkWQq7F5Oa7AGxuDsoS5UyYmKAoSzQ6ipmhWog3HjLsGZBAAjDDcyanBCEgiZQSrvp0mmaiLrAXMjwu66Cfr5CbiXAH6W03JNTkhjr2yP6Aq4XZZiXhFy4E5uftZGeH4XBIubREjJFXu7vY5CRlu83rXk+D42PPZiTpYwPYgttz8M0LGEYYcbAEVPVq5OakK1CCOA2jB/DdV/Ct/QDF5xCewYMps84/ZkT3HCHkECwC0V3JzJNZMSHxUvp5AJ/NgltDFd9D+RGsCW4ArZNmgnOYAM6girB+/C5M7+P8Kyx7g3PV4JwbnCvofvkezm8AGhhzCI1do8sAAAAASUVORK5CYII= 16 | 17 | // ==/UserScript== 18 | 19 | (function() { 20 | 'use strict'; 21 | 22 | // 配置 23 | const CONFIG = { 24 | API_BASE: 'https://www.deno.dev', 25 | STORAGE_KEY: 'password_manager_token', 26 | AUTO_SAVE: true, 27 | AUTO_FILL: true, 28 | SHOW_NOTIFICATIONS: true, 29 | DETECT_PASSWORD_CHANGE: true, 30 | DEBUG_MODE: false // 添加调试模式控制 31 | }; 32 | 33 | // 全局变量 34 | let authToken = GM_getValue(CONFIG.STORAGE_KEY, ''); 35 | let currentUser = null; 36 | let isAuthenticated = false; 37 | let detectedForms = []; 38 | let passwordManagerUI = null; 39 | let isPasswordManagerSite = false; 40 | let cachedMatches = []; 41 | let lastSubmittedData = null; 42 | let floatingButton = null; 43 | let authVerified = false; 44 | 45 | // 密码更新检测相关变量 46 | let passwordFieldWatchers = new Map(); 47 | let lastDetectedCredentials = new Map(); 48 | let pendingUpdates = new Map(); 49 | let updateRetryCount = new Map(); 50 | let pageUnloadHandler = null; 51 | 52 | // 登录状态检测相关变量 53 | let loginAttempts = new Map(); 54 | let loginStatusWatcher = null; 55 | let initialPageState = null; 56 | let loginSuccessPatterns = []; 57 | let loginFailurePatterns = []; 58 | let isMonitoringLogin = false; 59 | 60 | // 快速更新相关变量 61 | let preCheckedCredentials = new Map(); 62 | let fastUpdateQueue = []; 63 | let isProcessingFastUpdate = false; 64 | 65 | // 添加监听器状态控制 66 | let isPasswordWatchingActive = false; 67 | let lastFieldDetectionTime = 0; 68 | let fieldDetectionCooldown = 2000; // 2秒冷却时间 69 | 70 | // 调试日志函数 71 | function debugLog(message, ...args) { 72 | if (CONFIG.DEBUG_MODE) { 73 | console.log(message, ...args); 74 | } 75 | } 76 | 77 | // 重要日志函数(总是显示) 78 | function importantLog(message, ...args) { 79 | console.log(message, ...args); 80 | } 81 | 82 | // ========== 修复的快速密码更新系统 ========== 83 | 84 | // 预检查登录凭据 85 | async function preCheckLoginCredentials(username, password) { 86 | const credentialKey = `${window.location.hostname}_${username}`; 87 | 88 | debugLog('🚀 预检查登录凭据:', username.substring(0, 3) + '***'); 89 | 90 | try { 91 | // 异步预检查,不阻塞登录流程 92 | const checkPromise = makeRequest('/api/detect-login', { 93 | method: 'POST', 94 | headers: { 95 | 'Content-Type': 'application/json', 96 | 'Authorization': 'Bearer ' + authToken 97 | }, 98 | body: JSON.stringify({ 99 | url: window.location.href, 100 | username: username, 101 | password: password 102 | }) 103 | }); 104 | 105 | // 将预检查结果缓存 106 | preCheckedCredentials.set(credentialKey, { 107 | username: username, 108 | password: password, 109 | url: window.location.href, 110 | checkPromise: checkPromise, 111 | timestamp: Date.now() 112 | }); 113 | 114 | debugLog('✅ 预检查已启动:', credentialKey); 115 | 116 | } catch (error) { 117 | console.error('预检查失败:', error); 118 | } 119 | } 120 | 121 | // 快速执行密码更新 - 修复版本 122 | async function executeFastPasswordUpdate(username) { 123 | if (isProcessingFastUpdate) { 124 | debugLog('⚠️ 快速更新正在进行中,跳过重复执行'); 125 | return; 126 | } 127 | 128 | isProcessingFastUpdate = true; 129 | const credentialKey = `${window.location.hostname}_${username}`; 130 | 131 | debugLog('⚡ 开始快速密码更新:', username.substring(0, 3) + '***'); 132 | 133 | try { 134 | const preChecked = preCheckedCredentials.get(credentialKey); 135 | 136 | if (preChecked) { 137 | debugLog('⚡ 使用预检查数据执行更新'); 138 | 139 | try { 140 | // 等待预检查结果 141 | const response = await preChecked.checkPromise; 142 | debugLog('⚡ 预检查结果:', response); 143 | 144 | if (response.exists && response.passwordChanged) { 145 | debugLog('⚡ 确认密码变更,执行快速更新'); 146 | await updateExistingPasswordFast(response.existing.id, preChecked.password); 147 | showNotification('⚡ 密码已快速更新', 'success'); 148 | 149 | } else if (response.saved) { 150 | debugLog('⚡ 新账户已快速保存'); 151 | showNotification('⚡ 新账户已快速保存', 'success'); 152 | cachedMatches = []; 153 | 154 | } else if (response.exists && response.identical) { 155 | debugLog('ℹ️ 密码未变化'); 156 | showNotification('ℹ️ 密码未变化,无需更新', 'info'); 157 | } else { 158 | debugLog('⚠️ 未知响应状态,执行直接检查:', response); 159 | await executeDirectPasswordUpdate(username, credentialKey); 160 | } 161 | } catch (error) { 162 | console.error('预检查结果处理失败:', error); 163 | await executeDirectPasswordUpdate(username, credentialKey); 164 | } 165 | } else { 166 | debugLog('❌ 未找到预检查数据,执行直接检查'); 167 | await executeDirectPasswordUpdate(username, credentialKey); 168 | } 169 | 170 | } catch (error) { 171 | console.error('快速密码更新失败:', error); 172 | // 失败时加入重试队列 173 | fastUpdateQueue.push({ 174 | username: username, 175 | timestamp: Date.now() 176 | }); 177 | } finally { 178 | isProcessingFastUpdate = false; 179 | // 清理预检查数据 180 | preCheckedCredentials.delete(credentialKey); 181 | } 182 | } 183 | 184 | // 直接执行密码更新检查 - 修复版本 185 | async function executeDirectPasswordUpdate(username, credentialKey) { 186 | debugLog('🔄 执行直接密码更新检查:', username.substring(0, 3) + '***'); 187 | 188 | // 从登录尝试中获取密码 189 | let password = null; 190 | for (const [key, attempt] of loginAttempts.entries()) { 191 | if (attempt.username === username && (attempt.status === 'success' || attempt.status === 'pending')) { 192 | password = attempt.password; 193 | break; 194 | } 195 | } 196 | 197 | if (!password) { 198 | console.error('❌ 未找到对应的密码'); 199 | return; 200 | } 201 | 202 | try { 203 | const response = await makeRequest('/api/detect-login', { 204 | method: 'POST', 205 | headers: { 206 | 'Content-Type': 'application/json', 207 | 'Authorization': 'Bearer ' + authToken 208 | }, 209 | body: JSON.stringify({ 210 | url: window.location.href, 211 | username: username, 212 | password: password 213 | }) 214 | }); 215 | 216 | debugLog('🔄 直接检查结果:', response); 217 | 218 | if (response.exists && response.passwordChanged) { 219 | debugLog('🔄 确认密码变更,执行更新'); 220 | await updateExistingPasswordFast(response.existing.id, password); 221 | showNotification('✅ 密码已更新', 'success'); 222 | 223 | } else if (response.saved) { 224 | debugLog('✅ 新账户已保存'); 225 | showNotification('✅ 新账户已保存', 'success'); 226 | cachedMatches = []; 227 | 228 | } else if (response.exists && response.identical) { 229 | debugLog('ℹ️ 密码未变化'); 230 | showNotification('ℹ️ 密码未变化,无需更新', 'info'); 231 | } else { 232 | debugLog('⚠️ 未知响应状态:', response); 233 | } 234 | } catch (error) { 235 | console.error('直接检查失败:', error); 236 | throw error; 237 | } 238 | } 239 | 240 | // 快速更新密码(不等待响应)- 修复版本 241 | async function updateExistingPasswordFast(passwordId, newPassword) { 242 | debugLog('⚡ 快速更新密码:', passwordId); 243 | 244 | try { 245 | const response = await makeRequest(`/api/update-existing-password`, { 246 | method: 'POST', 247 | headers: { 248 | 'Content-Type': 'application/json', 249 | 'Authorization': 'Bearer ' + authToken 250 | }, 251 | body: JSON.stringify({ 252 | passwordId: passwordId, 253 | newPassword: newPassword 254 | }) 255 | }); 256 | 257 | debugLog('✅ 快速密码更新成功:', passwordId, response); 258 | cachedMatches = []; 259 | return response; 260 | 261 | } catch (error) { 262 | console.error('快速密码更新失败:', error); 263 | // 失败时重试 264 | setTimeout(() => { 265 | debugLog('🔄 重试密码更新:', passwordId); 266 | updateExistingPasswordFast(passwordId, newPassword); 267 | }, 2000); 268 | throw error; 269 | } 270 | } 271 | 272 | // 处理快速更新队列 273 | async function processFastUpdateQueue() { 274 | if (fastUpdateQueue.length === 0) return; 275 | 276 | debugLog('🔄 处理快速更新队列,待处理:', fastUpdateQueue.length); 277 | 278 | const queueCopy = [...fastUpdateQueue]; 279 | fastUpdateQueue = []; 280 | 281 | for (const item of queueCopy) { 282 | try { 283 | await executeFastPasswordUpdate(item.username); 284 | } catch (error) { 285 | console.error('处理快速更新队列失败:', error); 286 | } 287 | } 288 | } 289 | 290 | // ========== 修复的登录状态检测系统 ========== 291 | 292 | // 初始化登录状态检测 293 | function initLoginStatusDetection() { 294 | if (isMonitoringLogin) { 295 | debugLog('🔍 登录状态检测已在运行中'); 296 | return; 297 | } 298 | 299 | debugLog('🔍 初始化快速登录状态检测系统'); 300 | isMonitoringLogin = true; 301 | 302 | // 记录初始页面状态 303 | captureInitialPageState(); 304 | 305 | // 设置登录成功/失败检测模式 306 | setupLoginPatterns(); 307 | 308 | // 启动快速登录状态监听 309 | startFastLoginStatusWatching(); 310 | } 311 | 312 | // 捕获初始页面状态 313 | function captureInitialPageState() { 314 | initialPageState = { 315 | url: window.location.href, 316 | pathname: window.location.pathname, 317 | title: document.title, 318 | timestamp: Date.now(), 319 | hasLoginForm: detectedForms.length > 0, 320 | bodyText: document.body.textContent.toLowerCase(), 321 | errorElements: document.querySelectorAll('.error, .alert-danger, .alert-error, [class*="error"], [class*="fail"], .invalid-feedback').length, 322 | hasUserMenu: !!document.querySelector('.user-menu, .profile-menu, [href*="logout"], [href*="signout"], .logout'), 323 | hasWelcomeText: /welcome|欢迎|dashboard|控制台/.test(document.body.textContent.toLowerCase()) 324 | }; 325 | 326 | debugLog('📸 已捕获初始页面状态:', initialPageState); 327 | } 328 | 329 | // 设置登录检测模式 330 | function setupLoginPatterns() { 331 | // 登录成功的常见模式 332 | loginSuccessPatterns = [ 333 | // URL变化模式 334 | { 335 | type: 'url_change', 336 | patterns: [ 337 | /\/dashboard/i, 338 | /\/home/i, 339 | /\/profile/i, 340 | /\/account/i, 341 | /\/welcome/i, 342 | /\/main/i, 343 | /\/index(?!\.html?$)/i, 344 | /\/user/i, 345 | /\/member/i, 346 | /\/admin/i, 347 | /\/console/i, 348 | /\/panel/i 349 | ] 350 | }, 351 | // 页面内容模式 352 | { 353 | type: 'content', 354 | patterns: [ 355 | /welcome\s+back/i, 356 | /successfully\s+logged/i, 357 | /login\s+successful/i, 358 | /dashboard/i, 359 | /logout/i, 360 | /sign\s+out/i, 361 | /退出登录/i, 362 | /注销/i, 363 | /欢迎回来/i, 364 | /登录成功/i, 365 | /控制台/i, 366 | /个人中心/i, 367 | /我的账户/i, 368 | /用户中心/i, 369 | /管理面板/i 370 | ] 371 | }, 372 | // DOM元素模式 373 | { 374 | type: 'elements', 375 | selectors: [ 376 | '.user-menu', 377 | '.profile-menu', 378 | '.logout-btn', 379 | '.signout-btn', 380 | '[href*="logout"]', 381 | '[href*="signout"]', 382 | '[href*="sign-out"]', 383 | '.dashboard', 384 | '.user-info', 385 | '.user-profile', 386 | '.avatar', 387 | '.user-avatar', 388 | '.account-menu', 389 | '.header-user', 390 | '.nav-user', 391 | '.user-dropdown', 392 | '.profile-dropdown' 393 | ] 394 | } 395 | ]; 396 | 397 | // 登录失败的常见模式 398 | loginFailurePatterns = [ 399 | // 错误消息模式 400 | { 401 | type: 'error_content', 402 | patterns: [ 403 | /invalid.*password/i, 404 | /incorrect.*password/i, 405 | /wrong.*password/i, 406 | /invalid.*credentials/i, 407 | /authentication.*failed/i, 408 | /login.*failed/i, 409 | /access.*denied/i, 410 | /unauthorized/i, 411 | /用户名.*密码.*错误/i, 412 | /密码.*错误/i, 413 | /密码.*不正确/i, 414 | /登录.*失败/i, 415 | /认证.*失败/i, 416 | /账号.*密码.*不正确/i, 417 | /用户名.*不存在/i, 418 | /账户.*不存在/i, 419 | /验证.*失败/i 420 | ] 421 | }, 422 | // 错误元素模式 423 | { 424 | type: 'error_elements', 425 | selectors: [ 426 | '.error', 427 | '.alert-danger', 428 | '.alert-error', 429 | '.login-error', 430 | '.auth-error', 431 | '.form-error', 432 | '[class*="error"]', 433 | '[class*="fail"]', 434 | '[class*="invalid"]', 435 | '.invalid-feedback', 436 | '.field-error', 437 | '.input-error', 438 | '.message-error', 439 | '.notification-error', 440 | '.toast-error' 441 | ] 442 | } 443 | ]; 444 | 445 | debugLog('🎯 已设置登录检测模式'); 446 | } 447 | 448 | // 启动快速登录状态监听 449 | function startFastLoginStatusWatching() { 450 | debugLog('⚡ 启动快速登录状态监听器'); 451 | 452 | // 清理旧的监听器 453 | if (loginStatusWatcher) { 454 | loginStatusWatcher.cleanup(); 455 | } 456 | 457 | // 超高频URL监听 - 100ms检查一次 458 | let lastUrl = window.location.href; 459 | let lastPathname = window.location.pathname; 460 | 461 | const urlWatcher = setInterval(() => { 462 | const currentUrl = window.location.href; 463 | const currentPathname = window.location.pathname; 464 | 465 | if (currentUrl !== lastUrl || currentPathname !== lastPathname) { 466 | debugLog('⚡ 检测到URL快速变化:', { 467 | from: lastUrl, 468 | to: currentUrl, 469 | pathChanged: lastPathname !== currentPathname 470 | }); 471 | 472 | lastUrl = currentUrl; 473 | lastPathname = currentPathname; 474 | 475 | // URL变化立即检查登录状态 476 | checkLoginStatusFast('url_change'); 477 | 478 | // 如果是明显的成功跳转,立即执行快速更新 479 | if (isObviousSuccessRedirect(currentUrl, currentPathname)) { 480 | handleLoginSuccessFast('obvious_redirect'); 481 | } 482 | } 483 | }, 100); 484 | 485 | // 实时DOM监听 486 | const domWatcher = new MutationObserver((mutations) => { 487 | let shouldCheckFast = false; 488 | let hasSuccessIndicator = false; 489 | let hasErrorIndicator = false; 490 | 491 | mutations.forEach((mutation) => { 492 | if (mutation.type === 'childList') { 493 | mutation.addedNodes.forEach((node) => { 494 | if (node.nodeType === Node.ELEMENT_NODE) { 495 | const element = node; 496 | 497 | // 检查成功指示器 498 | if (element.classList && ( 499 | element.classList.contains('user-menu') || 500 | element.classList.contains('logout') || 501 | element.classList.contains('dashboard') || 502 | element.classList.contains('welcome') || 503 | element.querySelector && element.querySelector('.user-menu, [href*="logout"], .dashboard, .welcome') 504 | )) { 505 | hasSuccessIndicator = true; 506 | shouldCheckFast = true; 507 | } 508 | 509 | // 检查错误指示器 510 | if (element.classList && ( 511 | element.classList.contains('error') || 512 | element.classList.contains('alert-danger') || 513 | element.classList.contains('login-error') || 514 | element.querySelector && element.querySelector('.error, .alert-danger, .login-error') 515 | )) { 516 | hasErrorIndicator = true; 517 | shouldCheckFast = true; 518 | } 519 | 520 | // 检查文本内容 521 | if (element.textContent) { 522 | const text = element.textContent.toLowerCase(); 523 | if (text.includes('welcome') || text.includes('dashboard') || 524 | text.includes('logout') || text.includes('欢迎') || 525 | text.includes('控制台')) { 526 | hasSuccessIndicator = true; 527 | shouldCheckFast = true; 528 | } 529 | 530 | if (text.includes('error') || text.includes('failed') || 531 | text.includes('错误') || text.includes('失败')) { 532 | hasErrorIndicator = true; 533 | shouldCheckFast = true; 534 | } 535 | } 536 | } 537 | }); 538 | } 539 | }); 540 | 541 | if (shouldCheckFast) { 542 | debugLog('⚡ DOM变化触发快速检查', { hasSuccessIndicator, hasErrorIndicator }); 543 | 544 | if (hasSuccessIndicator) { 545 | handleLoginSuccessFast('success_indicator'); 546 | } else if (hasErrorIndicator) { 547 | handleLoginFailureFast('error_indicator'); 548 | } else { 549 | setTimeout(() => checkLoginStatusFast('dom_change'), 50); 550 | } 551 | } 552 | }); 553 | 554 | domWatcher.observe(document.body, { 555 | childList: true, 556 | subtree: true, 557 | characterData: true 558 | }); 559 | 560 | // 页面卸载前的最后检查 561 | const beforeUnloadHandler = () => { 562 | debugLog('⚡ 页面即将卸载,执行最后的快速检查'); 563 | handleLoginSuccessFast('page_unload'); 564 | }; 565 | 566 | window.addEventListener('beforeunload', beforeUnloadHandler); 567 | window.addEventListener('pagehide', beforeUnloadHandler); 568 | 569 | loginStatusWatcher = { 570 | urlWatcher, 571 | domWatcher, 572 | beforeUnloadHandler, 573 | cleanup: () => { 574 | clearInterval(urlWatcher); 575 | domWatcher.disconnect(); 576 | window.removeEventListener('beforeunload', beforeUnloadHandler); 577 | window.removeEventListener('pagehide', beforeUnloadHandler); 578 | } 579 | }; 580 | 581 | // 立即进行初始检查 582 | setTimeout(() => checkLoginStatusFast('initial'), 50); 583 | 584 | debugLog('⚡ 快速登录状态监听器已启动'); 585 | } 586 | 587 | // 检查是否是明显的成功重定向 588 | function isObviousSuccessRedirect(currentUrl, currentPathname) { 589 | const successPatterns = [ 590 | /\/dashboard/i, 591 | /\/home/i, 592 | /\/profile/i, 593 | /\/account/i, 594 | /\/welcome/i, 595 | /\/main/i, 596 | /\/admin/i, 597 | /\/console/i, 598 | /\/panel/i 599 | ]; 600 | 601 | for (const pattern of successPatterns) { 602 | if (pattern.test(currentUrl) || pattern.test(currentPathname)) { 603 | return true; 604 | } 605 | } 606 | 607 | if (initialPageState && 608 | initialPageState.url.match(/login|signin|auth/i) && 609 | !currentUrl.match(/login|signin|auth|register|signup/i)) { 610 | return true; 611 | } 612 | 613 | return false; 614 | } 615 | 616 | // 快速检查登录状态 617 | function checkLoginStatusFast(trigger = 'unknown') { 618 | if (!isMonitoringLogin || loginAttempts.size === 0) { 619 | return; 620 | } 621 | 622 | debugLog(`⚡ 快速检查登录状态 (触发: ${trigger})`); 623 | 624 | const currentState = { 625 | url: window.location.href, 626 | pathname: window.location.pathname, 627 | title: document.title, 628 | bodyText: document.body.textContent.toLowerCase(), 629 | timestamp: Date.now(), 630 | hasUserMenu: !!document.querySelector('.user-menu, .profile-menu, [href*="logout"], [href*="signout"], .logout'), 631 | hasWelcomeText: /welcome|欢迎|dashboard|控制台/.test(document.body.textContent.toLowerCase()) 632 | }; 633 | 634 | const loginSuccess = detectLoginSuccessFast(currentState, trigger); 635 | const loginFailure = detectLoginFailureFast(currentState, trigger); 636 | 637 | debugLog('⚡ 快速登录状态检查结果:', { 638 | success: loginSuccess, 639 | failure: loginFailure, 640 | trigger: trigger, 641 | pendingAttempts: loginAttempts.size 642 | }); 643 | 644 | if (loginSuccess) { 645 | handleLoginSuccessFast(trigger); 646 | } else if (loginFailure) { 647 | handleLoginFailureFast(trigger); 648 | } 649 | } 650 | 651 | // 快速检测登录成功 652 | function detectLoginSuccessFast(currentState, trigger) { 653 | debugLog('⚡ 快速检测登录成功'); 654 | 655 | // 1. URL明显变化检测 656 | if (currentState.pathname !== initialPageState.pathname) { 657 | for (const pattern of loginSuccessPatterns[0].patterns) { 658 | if (pattern.test(currentState.url) || pattern.test(currentState.pathname)) { 659 | debugLog('⚡ 通过URL快速检测到登录成功:', pattern); 660 | return true; 661 | } 662 | } 663 | 664 | // 离开登录页面 665 | if (!currentState.pathname.match(/login|signin|auth|register|signup/i) && 666 | initialPageState.url.match(/login|signin|auth/i)) { 667 | debugLog('⚡ 通过离开登录页快速检测到登录成功'); 668 | return true; 669 | } 670 | } 671 | 672 | // 2. 用户菜单出现 673 | if (currentState.hasUserMenu && !initialPageState.hasUserMenu) { 674 | debugLog('⚡ 通过用户菜单快速检测到登录成功'); 675 | return true; 676 | } 677 | 678 | // 3. 欢迎文本出现 679 | if (currentState.hasWelcomeText && !initialPageState.hasWelcomeText) { 680 | debugLog('⚡ 通过欢迎文本快速检测到登录成功'); 681 | return true; 682 | } 683 | 684 | // 4. DOM元素检测 685 | for (const selector of loginSuccessPatterns[2].selectors) { 686 | if (document.querySelector(selector)) { 687 | debugLog('⚡ 通过DOM元素快速检测到登录成功:', selector); 688 | return true; 689 | } 690 | } 691 | 692 | // 5. 页面标题变化 693 | if (currentState.title !== initialPageState.title) { 694 | const titleLower = currentState.title.toLowerCase(); 695 | if (titleLower.includes('dashboard') || titleLower.includes('welcome') || 696 | titleLower.includes('home') || titleLower.includes('控制台') || 697 | titleLower.includes('欢迎')) { 698 | debugLog('⚡ 通过标题变化快速检测到登录成功'); 699 | return true; 700 | } 701 | } 702 | 703 | return false; 704 | } 705 | 706 | // 快速检测登录失败 707 | function detectLoginFailureFast(currentState, trigger) { 708 | debugLog('⚡ 快速检测登录失败'); 709 | 710 | // 1. 错误消息检测 711 | for (const pattern of loginFailurePatterns[0].patterns) { 712 | if (pattern.test(currentState.bodyText)) { 713 | debugLog('⚡ 通过错误消息快速检测到登录失败:', pattern); 714 | return true; 715 | } 716 | } 717 | 718 | // 2. 错误元素检测 719 | for (const selector of loginFailurePatterns[1].selectors) { 720 | const errorElements = document.querySelectorAll(selector); 721 | if (errorElements.length > 0) { 722 | for (const element of errorElements) { 723 | if (element.offsetParent !== null && element.textContent.trim()) { 724 | debugLog('⚡ 通过错误元素快速检测到登录失败:', selector); 725 | return true; 726 | } 727 | } 728 | } 729 | } 730 | 731 | return false; 732 | } 733 | 734 | // 快速处理登录成功 - 修复版本(关键修复:避免显示多余通知) 735 | async function handleLoginSuccessFast(trigger = 'unknown') { 736 | importantLog(`✅ 快速处理登录成功!(触发: ${trigger})`); 737 | 738 | let updatedCount = 0; 739 | const updatePromises = []; 740 | let hasPendingAttempts = false; 741 | 742 | // 检查是否有待处理的登录尝试 743 | for (const [key, attempt] of loginAttempts.entries()) { 744 | if (attempt.status === 'pending') { 745 | hasPendingAttempts = true; 746 | break; 747 | } 748 | } 749 | 750 | // 只有在有待处理的登录尝试时才显示通知和执行更新 751 | if (!hasPendingAttempts) { 752 | debugLog('ℹ️ 没有待处理的登录尝试,跳过密码更新'); 753 | cleanupLoginStatusWatcher(); 754 | return; 755 | } 756 | 757 | showNotification('🎉 检测到登录成功,正在更新密码...', 'success'); 758 | 759 | // 立即执行所有待处理的快速更新 760 | for (const [key, attempt] of loginAttempts.entries()) { 761 | if (attempt.status === 'pending') { 762 | debugLog('⚡ 登录成功,执行快速密码更新:', attempt.username.substring(0, 3) + '***'); 763 | attempt.status = 'success'; 764 | 765 | // 创建更新Promise 766 | const updatePromise = (async () => { 767 | try { 768 | await executeFastPasswordUpdate(attempt.username); 769 | updatedCount++; 770 | debugLog(`✅ 密码更新完成 ${updatedCount}`); 771 | } catch (error) { 772 | console.error('密码更新失败:', error); 773 | // 即使失败也要显示通知 774 | showNotification(`❌ 密码更新失败: ${attempt.username.substring(0, 3)}***`, 'error'); 775 | } 776 | })(); 777 | 778 | updatePromises.push(updatePromise); 779 | } 780 | } 781 | 782 | // 等待所有更新完成 783 | if (updatePromises.length > 0) { 784 | try { 785 | debugLog(`⚡ 等待 ${updatePromises.length} 个密码更新完成...`); 786 | const results = await Promise.allSettled(updatePromises); 787 | 788 | // 统计成功和失败的数量 789 | const successCount = results.filter(r => r.status === 'fulfilled').length; 790 | const failureCount = results.filter(r => r.status === 'rejected').length; 791 | 792 | debugLog(`⚡ 密码更新完成统计: 成功 ${successCount}, 失败 ${failureCount}`); 793 | 794 | if (successCount > 0) { 795 | showNotification(`✅ 已成功更新 ${successCount} 个密码`, 'success'); 796 | } 797 | 798 | if (failureCount > 0) { 799 | showNotification(`⚠️ ${failureCount} 个密码更新失败`, 'warning'); 800 | } 801 | 802 | } catch (error) { 803 | console.error('批量更新失败:', error); 804 | showNotification('⚠️ 密码更新过程中发生错误', 'warning'); 805 | } 806 | } 807 | 808 | // 清理监听器 809 | cleanupLoginStatusWatcher(); 810 | } 811 | 812 | // 快速处理登录失败 - 修复版本 813 | function handleLoginFailureFast(trigger = 'unknown') { 814 | debugLog(`❌ 快速处理登录失败!(触发: ${trigger})`); 815 | 816 | let hasFailedAttempts = false; 817 | 818 | // 检查是否有待处理的登录尝试 819 | for (const [key, attempt] of loginAttempts.entries()) { 820 | if (attempt.status === 'pending') { 821 | hasFailedAttempts = true; 822 | break; 823 | } 824 | } 825 | 826 | // 只有在有待处理的登录尝试时才显示通知 827 | if (hasFailedAttempts) { 828 | showNotification('❌ 检测到登录失败,不会更新密码', 'warning'); 829 | 830 | // 标记所有待更新的密码为失败 831 | loginAttempts.forEach((attempt, key) => { 832 | if (attempt.status === 'pending') { 833 | debugLog('❌ 登录失败,取消密码更新:', attempt.username.substring(0, 3) + '***'); 834 | attempt.status = 'failed'; 835 | } 836 | }); 837 | } else { 838 | debugLog('ℹ️ 没有待处理的登录尝试,跳过失败处理'); 839 | } 840 | 841 | // 清理监听器 842 | cleanupLoginStatusWatcher(); 843 | } 844 | 845 | // 清理登录状态监听器 846 | function cleanupLoginStatusWatcher() { 847 | debugLog('🧹 清理登录状态监听器'); 848 | 849 | if (loginStatusWatcher) { 850 | loginStatusWatcher.cleanup(); 851 | loginStatusWatcher = null; 852 | } 853 | 854 | isMonitoringLogin = false; 855 | 856 | // 处理剩余的快速更新队列 857 | setTimeout(() => { 858 | processFastUpdateQueue(); 859 | }, 1000); 860 | 861 | // 清理过期的登录尝试记录 862 | setTimeout(() => { 863 | loginAttempts.clear(); 864 | preCheckedCredentials.clear(); 865 | debugLog('🧹 已清理登录尝试记录'); 866 | }, 5000); 867 | } 868 | 869 | // ========== 其余代码保持不变 ========== 870 | 871 | // 全局函数定义 872 | function fillPasswordFromElement(buttonElement) { 873 | debugLog('🔐 fillPasswordFromElement 被调用', buttonElement); 874 | try { 875 | const passwordItem = buttonElement.closest('.pm-password-item'); 876 | if (!passwordItem) { 877 | console.error('❌ 找不到 .pm-password-item 元素'); 878 | showNotification('❌ 填充失败:找不到密码项', 'error'); 879 | return; 880 | } 881 | 882 | const matchDataStr = passwordItem.getAttribute('data-match'); 883 | if (!matchDataStr) { 884 | console.error('❌ 找不到 data-match 属性'); 885 | showNotification('❌ 填充失败:找不到密码数据', 'error'); 886 | return; 887 | } 888 | 889 | const matchData = JSON.parse(matchDataStr); 890 | debugLog('🔐 解析密码数据成功:', matchData); 891 | 892 | fillPassword(matchData); 893 | } catch (error) { 894 | console.error('❌ fillPasswordFromElement 执行失败:', error); 895 | showNotification('❌ 填充失败', 'error'); 896 | } 897 | } 898 | 899 | // 更新现有密码 - 增强版本,支持重试 900 | async function updateExistingPassword(passwordId, newPassword, retryCount = 0) { 901 | debugLog('🔄 自动更新密码', passwordId, '重试次数:', retryCount); 902 | 903 | try { 904 | const response = await makeRequest(`/api/update-existing-password`, { 905 | method: 'POST', 906 | headers: { 907 | 'Content-Type': 'application/json', 908 | 'Authorization': 'Bearer ' + authToken 909 | }, 910 | body: JSON.stringify({ 911 | passwordId: passwordId, 912 | newPassword: newPassword 913 | }) 914 | }); 915 | 916 | showNotification('✅ 密码已自动更新,历史记录已保存', 'success'); 917 | 918 | // 清除缓存和重试记录 919 | cachedMatches = []; 920 | updateRetryCount.delete(passwordId); 921 | pendingUpdates.delete(passwordId); 922 | 923 | debugLog('✅ 密码更新成功:', passwordId); 924 | 925 | } catch (error) { 926 | console.error('更新密码失败:', error); 927 | 928 | // 重试逻辑 929 | if (retryCount < 3) { 930 | debugLog(`🔄 密码更新失败,准备重试 ${retryCount + 1}/3`); 931 | updateRetryCount.set(passwordId, retryCount + 1); 932 | 933 | // 延迟重试 934 | setTimeout(() => { 935 | updateExistingPassword(passwordId, newPassword, retryCount + 1); 936 | }, (retryCount + 1) * 2000); 937 | } else { 938 | showNotification('❌ 密码更新失败,已达到最大重试次数', 'error'); 939 | updateRetryCount.delete(passwordId); 940 | pendingUpdates.delete(passwordId); 941 | } 942 | } 943 | } 944 | 945 | // 查看密码历史 946 | async function viewPasswordHistory(passwordId) { 947 | try { 948 | const response = await makeRequest(`/api/passwords/${passwordId}/history`, { 949 | method: 'GET', 950 | headers: { 951 | 'Authorization': 'Bearer ' + authToken 952 | } 953 | }); 954 | 955 | showPasswordHistoryModal(response.history, passwordId); 956 | } catch (error) { 957 | console.error('获取密码历史失败:', error); 958 | showNotification('❌ 获取密码历史失败', 'error'); 959 | } 960 | } 961 | 962 | // 删除历史密码记录 963 | async function deleteHistoryEntry(passwordId, historyId) { 964 | if (!confirm('确定要删除这条历史记录吗?')) { 965 | return; 966 | } 967 | 968 | try { 969 | const response = await makeRequest('/api/passwords/delete-history', { 970 | method: 'POST', 971 | headers: { 972 | 'Content-Type': 'application/json', 973 | 'Authorization': 'Bearer ' + authToken 974 | }, 975 | body: JSON.stringify({ 976 | passwordId: passwordId, 977 | historyId: historyId 978 | }) 979 | }); 980 | 981 | if (response.success) { 982 | showNotification('🗑️ 历史记录已删除', 'success'); 983 | // 重新加载历史记录 984 | viewPasswordHistory(passwordId); 985 | } else { 986 | throw new Error(response.error || '删除失败'); 987 | } 988 | } catch (error) { 989 | console.error('删除历史记录失败:', error); 990 | showNotification('❌ 删除历史记录失败: ' + error.message, 'error'); 991 | } 992 | } 993 | 994 | // 删除所有历史记录 995 | async function deleteAllHistory(passwordId) { 996 | if (!confirm('确定要删除所有历史记录吗?此操作无法撤销。')) { 997 | return; 998 | } 999 | 1000 | try { 1001 | const response = await makeRequest('/api/passwords/delete-history', { 1002 | method: 'POST', 1003 | headers: { 1004 | 'Content-Type': 'application/json', 1005 | 'Authorization': 'Bearer ' + authToken 1006 | }, 1007 | body: JSON.stringify({ 1008 | passwordId: passwordId, 1009 | historyId: 'all' 1010 | }) 1011 | }); 1012 | 1013 | if (response.success) { 1014 | showNotification('🗑️ ' + response.message, 'success'); 1015 | // 重新加载历史记录 1016 | viewPasswordHistory(passwordId); 1017 | } else { 1018 | throw new Error(response.error || '删除失败'); 1019 | } 1020 | } catch (error) { 1021 | console.error('删除所有历史记录失败:', error); 1022 | showNotification('❌ 删除所有历史记录失败: ' + error.message, 'error'); 1023 | } 1024 | } 1025 | 1026 | // 显示密码历史模态框 1027 | function showPasswordHistoryModal(history, passwordId) { 1028 | const modal = document.createElement('div'); 1029 | modal.className = 'pm-password-history-modal'; 1030 | modal.innerHTML = ` 1031 |
1032 |
1033 |
1034 |

📜 密码历史记录

1035 |
1036 | ${history.length > 0 ? ` 1037 | 1040 | ` : ''} 1041 | 1044 |
1045 |
1046 |
1047 | ${history.length === 0 ? 1048 | '

暂无历史记录

' : 1049 | history.map((entry, index) => ` 1050 |
1051 |
1052 | ${new Date(entry.changedAt).toLocaleString()} 1053 |
1054 | 1057 | 1060 |
1061 |
1062 |
1063 | 1064 | •••••••• 1065 | 1068 |
1069 |
1070 | `).join('') 1071 | } 1072 |
1073 |
1074 |
1075 | `; 1076 | 1077 | document.body.appendChild(modal); 1078 | 1079 | // 事件委托监听 1080 | modal.addEventListener('click', (e) => { 1081 | const target = e.target; 1082 | 1083 | if (target.matches('.pm-modal-overlay') || target.closest('.pm-close-btn')) { 1084 | if (!target.closest('.pm-modal-content') || target.closest('.pm-close-btn')) { 1085 | modal.remove(); 1086 | return; 1087 | } 1088 | } 1089 | 1090 | const toggleButton = target.closest('.pm-btn-toggle-history-pwd'); 1091 | if (toggleButton) { 1092 | const elementId = toggleButton.dataset.elementId; 1093 | const password = toggleButton.dataset.password; 1094 | const element = document.getElementById(elementId); 1095 | const icon = toggleButton.querySelector('i'); 1096 | 1097 | if (element && icon) { 1098 | if (element.textContent === '••••••••') { 1099 | element.textContent = password; 1100 | icon.className = 'fas fa-eye-slash'; 1101 | } else { 1102 | element.textContent = '••••••••'; 1103 | icon.className = 'fas fa-eye'; 1104 | } 1105 | } 1106 | return; 1107 | } 1108 | 1109 | const restoreButton = target.closest('.pm-btn-restore'); 1110 | if (restoreButton) { 1111 | const passwordIdToRestore = restoreButton.dataset.passwordId; 1112 | const historyIdToRestore = restoreButton.dataset.historyId; 1113 | 1114 | if (!confirm('确定要恢复到这个历史密码吗?当前密码将被保存到历史记录中。')) { 1115 | return; 1116 | } 1117 | 1118 | makeRequest('/api/passwords/restore', { 1119 | method: 'POST', 1120 | headers: { 1121 | 'Content-Type': 'application/json', 1122 | 'Authorization': 'Bearer ' + authToken 1123 | }, 1124 | body: JSON.stringify({ passwordId: passwordIdToRestore, historyId: historyIdToRestore }) 1125 | }).then(() => { 1126 | showNotification('✅ 密码已恢复到历史版本', 'success'); 1127 | modal.remove(); 1128 | cachedMatches = []; // 清除缓存 1129 | }).catch(error => { 1130 | showNotification('❌ 恢复密码失败', 'error'); 1131 | console.error('恢复密码失败:', error); 1132 | }); 1133 | return; 1134 | } 1135 | 1136 | const deleteButton = target.closest('.pm-btn-delete-history'); 1137 | if (deleteButton) { 1138 | const passwordIdToDelete = deleteButton.dataset.passwordId; 1139 | const historyIdToDelete = deleteButton.dataset.historyId; 1140 | deleteHistoryEntry(passwordIdToDelete, historyIdToDelete); 1141 | return; 1142 | } 1143 | 1144 | const deleteAllButton = target.closest('.pm-btn-delete-all'); 1145 | if (deleteAllButton) { 1146 | const passwordIdToDelete = deleteAllButton.dataset.passwordId; 1147 | deleteAllHistory(passwordIdToDelete); 1148 | return; 1149 | } 1150 | }); 1151 | } 1152 | 1153 | // 主要填充函数 - 修复async问题 1154 | async function fillPassword(passwordData) { 1155 | debugLog('🔐 开始填充密码流程:', passwordData); 1156 | 1157 | try { 1158 | let username, password; 1159 | 1160 | if (typeof passwordData === 'object') { 1161 | username = passwordData.username; 1162 | password = passwordData.password; 1163 | } else { 1164 | username = arguments[1]; 1165 | password = arguments[2]; 1166 | } 1167 | 1168 | if (!username || !password) { 1169 | console.error('❌ 用户名或密码为空'); 1170 | showNotification('❌ 用户名或密码为空', 'error'); 1171 | return; 1172 | } 1173 | 1174 | debugLog('🔐 准备填充:', { 1175 | username: username?.substring(0, 3) + '***', 1176 | hasPassword: !!password 1177 | }); 1178 | 1179 | // 使用更精确的字段查找 1180 | const usernameFields = findUsernameFieldsAdvanced(); 1181 | const passwordFields = findPasswordFieldsAdvanced(); 1182 | 1183 | debugLog('🔍 找到字段:', { 1184 | usernameFields: usernameFields.length, 1185 | passwordFields: passwordFields.length, 1186 | usernameFieldsDetails: usernameFields.map(f => ({ 1187 | tag: f.tagName, 1188 | type: f.type, 1189 | name: f.name, 1190 | id: f.id, 1191 | className: f.className, 1192 | placeholder: f.placeholder 1193 | })), 1194 | passwordFieldsDetails: passwordFields.map(f => ({ 1195 | tag: f.tagName, 1196 | type: f.type, 1197 | name: f.name, 1198 | id: f.id, 1199 | className: f.className 1200 | })) 1201 | }); 1202 | 1203 | if (usernameFields.length === 0 && passwordFields.length === 0) { 1204 | console.warn('⚠️ 未找到任何可填充的字段'); 1205 | showNotification('⚠️ 未找到可填充的字段', 'warning'); 1206 | return; 1207 | } 1208 | 1209 | let filledFields = 0; 1210 | 1211 | // 填充用户名字段 1212 | if (usernameFields.length > 0 && username) { 1213 | debugLog('🔐 开始填充用户名字段...'); 1214 | for (let i = 0; i < usernameFields.length; i++) { 1215 | const field = usernameFields[i]; 1216 | debugLog(`🔐 尝试填充用户名字段 ${i + 1}:`, field); 1217 | 1218 | const success = await fillInputFieldAdvanced(field, username, '用户名'); 1219 | if (success) { 1220 | filledFields++; 1221 | debugLog(`✅ 用户名字段 ${i + 1} 填充成功`); 1222 | } else { 1223 | debugLog(`❌ 用户名字段 ${i + 1} 填充失败`); 1224 | } 1225 | } 1226 | } 1227 | 1228 | // 填充密码字段 1229 | if (passwordFields.length > 0 && password) { 1230 | debugLog('🔐 开始填充密码字段...'); 1231 | for (let i = 0; i < passwordFields.length; i++) { 1232 | const field = passwordFields[i]; 1233 | debugLog(`🔐 尝试填充密码字段 ${i + 1}:`, field); 1234 | 1235 | const success = await fillInputFieldAdvanced(field, password, '密码'); 1236 | if (success) { 1237 | filledFields++; 1238 | debugLog(`✅ 密码字段 ${i + 1} 填充成功`); 1239 | } else { 1240 | debugLog(`❌ 密码字段 ${i + 1} 填充失败`); 1241 | } 1242 | } 1243 | } 1244 | 1245 | // 显示结果 1246 | if (filledFields > 0) { 1247 | showNotification(`🔐 已填充 ${filledFields} 个字段`, 'success'); 1248 | importantLog(`✅ 填充完成,共填充 ${filledFields} 个字段`); 1249 | } else { 1250 | showNotification('⚠️ 填充失败,请检查页面字段', 'warning'); 1251 | console.warn('⚠️ 所有字段填充都失败了'); 1252 | } 1253 | 1254 | // 关闭弹窗 1255 | if (passwordManagerUI) { 1256 | passwordManagerUI.remove(); 1257 | passwordManagerUI = null; 1258 | } 1259 | 1260 | } catch (error) { 1261 | console.error('❌ 填充密码时发生错误:', error); 1262 | showNotification('❌ 填充密码失败', 'error'); 1263 | } 1264 | } 1265 | 1266 | // ========== 增强的密码更新检测系统 ========== 1267 | 1268 | // 启动密码字段监听 - 优化版本,避免重复调用 1269 | function startPasswordFieldWatching() { 1270 | // 检查冷却时间,避免频繁重复调用 1271 | const now = Date.now(); 1272 | if (isPasswordWatchingActive && (now - lastFieldDetectionTime) < fieldDetectionCooldown) { 1273 | debugLog('⚠️ 密码字段监听系统正在冷却中,跳过重复启动'); 1274 | return; 1275 | } 1276 | 1277 | if (isPasswordWatchingActive) { 1278 | debugLog('⚠️ 密码字段监听系统已在运行中,先清理再重启'); 1279 | cleanupPasswordFieldWatchers(); 1280 | } 1281 | 1282 | lastFieldDetectionTime = now; 1283 | isPasswordWatchingActive = true; 1284 | 1285 | debugLog('🔍 启动密码字段监听系统'); 1286 | 1287 | // 清理旧的监听器 1288 | passwordFieldWatchers.clear(); 1289 | 1290 | // 监听所有密码字段 1291 | const passwordFields = findPasswordFieldsAdvanced(); 1292 | const usernameFields = findUsernameFieldsAdvanced(); 1293 | 1294 | if (passwordFields.length === 0 && usernameFields.length === 0) { 1295 | debugLog('ℹ️ 未找到任何字段,跳过监听器设置'); 1296 | isPasswordWatchingActive = false; 1297 | return; 1298 | } 1299 | 1300 | passwordFields.forEach((passwordField, index) => { 1301 | watchPasswordField(passwordField, usernameFields, index); 1302 | }); 1303 | 1304 | // 设置页面卸载监听 1305 | setupPageUnloadHandler(); 1306 | 1307 | debugLog(`🔍 已设置 ${passwordFields.length} 个密码字段监听器`); 1308 | } 1309 | 1310 | // 监听单个密码字段 1311 | function watchPasswordField(passwordField, usernameFields, index) { 1312 | const fieldId = `pwd_${index}_${Date.now()}`; 1313 | 1314 | // 创建监听器对象 1315 | const watcher = { 1316 | field: passwordField, 1317 | usernameFields: usernameFields, 1318 | lastValue: '', 1319 | lastUsername: '', 1320 | changeTimer: null, 1321 | submitTimer: null 1322 | }; 1323 | 1324 | // 输入事件监听 1325 | const inputHandler = (e) => { 1326 | clearTimeout(watcher.changeTimer); 1327 | watcher.changeTimer = setTimeout(() => { 1328 | handlePasswordFieldChange(watcher); 1329 | }, 300); 1330 | }; 1331 | 1332 | // 失焦事件监听 1333 | const blurHandler = (e) => { 1334 | setTimeout(() => { 1335 | handlePasswordFieldChange(watcher); 1336 | }, 50); 1337 | }; 1338 | 1339 | // 键盘事件监听(回车键) 1340 | const keyHandler = (e) => { 1341 | if (e.key === 'Enter') { 1342 | setTimeout(() => { 1343 | handlePasswordFieldSubmit(watcher); 1344 | }, 50); 1345 | } 1346 | }; 1347 | 1348 | // 绑定事件 1349 | passwordField.addEventListener('input', inputHandler); 1350 | passwordField.addEventListener('blur', blurHandler); 1351 | passwordField.addEventListener('keydown', keyHandler); 1352 | 1353 | // 保存监听器 1354 | passwordFieldWatchers.set(fieldId, { 1355 | ...watcher, 1356 | inputHandler, 1357 | blurHandler, 1358 | keyHandler, 1359 | cleanup: () => { 1360 | passwordField.removeEventListener('input', inputHandler); 1361 | passwordField.removeEventListener('blur', blurHandler); 1362 | passwordField.removeEventListener('keydown', keyHandler); 1363 | clearTimeout(watcher.changeTimer); 1364 | clearTimeout(watcher.submitTimer); 1365 | } 1366 | }); 1367 | 1368 | debugLog(`🔍 已设置密码字段监听器: ${fieldId}`); 1369 | } 1370 | 1371 | // 处理密码字段变化 1372 | function handlePasswordFieldChange(watcher) { 1373 | if (!isAuthenticated || isPasswordManagerSite) return; 1374 | 1375 | const currentPassword = watcher.field.value; 1376 | const currentUsername = getCurrentUsername(watcher.usernameFields); 1377 | 1378 | // 检查是否有有效的凭据 1379 | if (!currentUsername || !currentPassword || currentPassword.length < 3) { 1380 | return; 1381 | } 1382 | 1383 | // 检查是否与上次记录的值相同 1384 | if (currentPassword === watcher.lastValue && currentUsername === watcher.lastUsername) { 1385 | return; 1386 | } 1387 | 1388 | debugLog('🔍 检测到密码字段变化:', { 1389 | username: currentUsername.substring(0, 3) + '***', 1390 | passwordLength: currentPassword.length, 1391 | hasChanged: currentPassword !== watcher.lastValue 1392 | }); 1393 | 1394 | // 更新记录 1395 | watcher.lastValue = currentPassword; 1396 | watcher.lastUsername = currentUsername; 1397 | 1398 | // 记录当前凭据 1399 | const credentialKey = `${window.location.hostname}_${currentUsername}`; 1400 | lastDetectedCredentials.set(credentialKey, { 1401 | username: currentUsername, 1402 | password: currentPassword, 1403 | timestamp: Date.now(), 1404 | url: window.location.href 1405 | }); 1406 | 1407 | // 预检查凭据(不阻塞用户操作) 1408 | preCheckLoginCredentials(currentUsername, currentPassword); 1409 | } 1410 | 1411 | // 处理密码字段提交 1412 | function handlePasswordFieldSubmit(watcher) { 1413 | if (!isAuthenticated || isPasswordManagerSite) return; 1414 | 1415 | const currentPassword = watcher.field.value; 1416 | const currentUsername = getCurrentUsername(watcher.usernameFields); 1417 | 1418 | if (!currentUsername || !currentPassword) return; 1419 | 1420 | debugLog('🔍 检测到密码字段提交事件'); 1421 | 1422 | // 记录登录尝试并启动快速状态检测 1423 | recordLoginAttemptFast(currentUsername, currentPassword); 1424 | } 1425 | 1426 | // 快速记录登录尝试 1427 | function recordLoginAttemptFast(username, password) { 1428 | const attemptKey = `${window.location.hostname}_${username}_${Date.now()}`; 1429 | 1430 | const attempt = { 1431 | username: username, 1432 | password: password, 1433 | url: window.location.href, 1434 | timestamp: Date.now(), 1435 | status: 'pending' // pending, success, failed 1436 | }; 1437 | 1438 | loginAttempts.set(attemptKey, attempt); 1439 | 1440 | debugLog('⚡ 快速记录登录尝试:', { 1441 | username: username.substring(0, 3) + '***', 1442 | url: attempt.url, 1443 | key: attemptKey 1444 | }); 1445 | 1446 | // 立即启动快速登录状态检测 1447 | initLoginStatusDetection(); 1448 | 1449 | // 设置快速超时处理(5秒后假设成功) 1450 | setTimeout(() => { 1451 | const currentAttempt = loginAttempts.get(attemptKey); 1452 | if (currentAttempt && currentAttempt.status === 'pending') { 1453 | debugLog('⚡ 快速超时,假设登录成功'); 1454 | handleLoginSuccessFast('fast_timeout'); 1455 | } 1456 | }, 5000); 1457 | } 1458 | 1459 | // 获取当前用户名 1460 | function getCurrentUsername(usernameFields) { 1461 | for (const field of usernameFields) { 1462 | if (field.value && field.value.trim()) { 1463 | return field.value.trim(); 1464 | } 1465 | } 1466 | return ''; 1467 | } 1468 | 1469 | // 设置页面卸载处理器 1470 | function setupPageUnloadHandler() { 1471 | // 清理旧的处理器 1472 | if (pageUnloadHandler) { 1473 | window.removeEventListener('beforeunload', pageUnloadHandler); 1474 | window.removeEventListener('pagehide', pageUnloadHandler); 1475 | } 1476 | 1477 | // 创建新的处理器 1478 | pageUnloadHandler = () => { 1479 | debugLog('⚡ 页面即将卸载,执行快速密码更新'); 1480 | 1481 | // 检查是否有待处理的登录尝试 1482 | loginAttempts.forEach((attempt, key) => { 1483 | if (attempt.status === 'pending') { 1484 | debugLog('⚡ 页面卸载时发现待处理的登录尝试,立即执行快速更新'); 1485 | // 页面跳转通常意味着登录成功,立即执行快速更新 1486 | attempt.status = 'success'; 1487 | executeFastPasswordUpdate(attempt.username); 1488 | } 1489 | }); 1490 | }; 1491 | 1492 | // 绑定事件 1493 | window.addEventListener('beforeunload', pageUnloadHandler); 1494 | window.addEventListener('pagehide', pageUnloadHandler); 1495 | } 1496 | 1497 | // 清理密码字段监听器 - 优化版本 1498 | function cleanupPasswordFieldWatchers() { 1499 | debugLog('🧹 清理密码字段监听器'); 1500 | 1501 | passwordFieldWatchers.forEach((watcher, id) => { 1502 | watcher.cleanup(); 1503 | }); 1504 | 1505 | passwordFieldWatchers.clear(); 1506 | isPasswordWatchingActive = false; 1507 | 1508 | if (pageUnloadHandler) { 1509 | window.removeEventListener('beforeunload', pageUnloadHandler); 1510 | window.removeEventListener('pagehide', pageUnloadHandler); 1511 | pageUnloadHandler = null; 1512 | } 1513 | 1514 | // 清理登录状态监听器 1515 | cleanupLoginStatusWatcher(); 1516 | } 1517 | 1518 | // 扩展对象 1519 | window.pmExtension = { 1520 | fillPassword: fillPassword, 1521 | 1522 | setToken: function() { 1523 | const token = document.getElementById('tokenInput').value.trim(); 1524 | if (token) { 1525 | authToken = token; 1526 | GM_setValue(CONFIG.STORAGE_KEY, token); 1527 | authVerified = false; 1528 | verifyAuth().then(() => { 1529 | if (passwordManagerUI) { 1530 | passwordManagerUI.remove(); 1531 | passwordManagerUI = null; 1532 | } 1533 | createPasswordManagerUI(); 1534 | }); 1535 | } 1536 | }, 1537 | 1538 | copyToken: function(token) { 1539 | try { 1540 | if (typeof GM_setClipboard !== 'undefined') { 1541 | GM_setClipboard(token); 1542 | showCopySuccess(); 1543 | showNotification('📋 令牌已复制到剪贴板', 'success'); 1544 | return; 1545 | } 1546 | 1547 | if (navigator.clipboard && navigator.clipboard.writeText) { 1548 | navigator.clipboard.writeText(token).then(() => { 1549 | showCopySuccess(); 1550 | showNotification('📋 令牌已复制到剪贴板', 'success'); 1551 | }).catch(() => { 1552 | fallbackCopy(token); 1553 | }); 1554 | } else { 1555 | fallbackCopy(token); 1556 | } 1557 | } catch (error) { 1558 | fallbackCopy(token); 1559 | } 1560 | }, 1561 | 1562 | refreshAuth: async function() { 1563 | authVerified = false; 1564 | await verifyAuth(); 1565 | showNotification('🔄 连接状态已刷新', 'info'); 1566 | if (passwordManagerUI) { 1567 | passwordManagerUI.remove(); 1568 | passwordManagerUI = null; 1569 | } 1570 | createPasswordManagerUI(); 1571 | }, 1572 | 1573 | highlightForms: function() { 1574 | detectedForms.forEach(form => { 1575 | const overlay = document.createElement('div'); 1576 | overlay.className = 'pm-form-overlay'; 1577 | 1578 | const rect = form.getBoundingClientRect(); 1579 | overlay.style.top = (rect.top + window.scrollY) + 'px'; 1580 | overlay.style.left = (rect.left + window.scrollX) + 'px'; 1581 | overlay.style.width = rect.width + 'px'; 1582 | overlay.style.height = rect.height + 'px'; 1583 | 1584 | document.body.appendChild(overlay); 1585 | 1586 | setTimeout(() => overlay.remove(), 3000); 1587 | }); 1588 | 1589 | showNotification('📍 登录表单已高亮显示', 'info'); 1590 | }, 1591 | 1592 | // 手动获取密码匹配(用户主动操作) 1593 | getPasswordMatches: async function() { 1594 | if (!isAuthenticated || isPasswordManagerSite) { 1595 | showNotification('❌ 未连接到密码管理器', 'error'); 1596 | return []; 1597 | } 1598 | 1599 | try { 1600 | const matches = await getPasswordMatches(); 1601 | cachedMatches = matches; 1602 | updateFloatingButton(matches); 1603 | return matches; 1604 | } catch (error) { 1605 | console.error('获取密码匹配失败:', error); 1606 | showNotification('❌ 获取密码匹配失败', 'error'); 1607 | return []; 1608 | } 1609 | }, 1610 | 1611 | // 切换调试模式 1612 | toggleDebugMode: function() { 1613 | CONFIG.DEBUG_MODE = !CONFIG.DEBUG_MODE; 1614 | showNotification(`🔧 调试模式已${CONFIG.DEBUG_MODE ? '开启' : '关闭'}`, 'info'); 1615 | importantLog(`🔧 调试模式已${CONFIG.DEBUG_MODE ? '开启' : '关闭'}`); 1616 | } 1617 | }; 1618 | 1619 | // ========== 工具函数 ========== 1620 | 1621 | // 检查是否是密码管理器网站 1622 | function checkPasswordManagerSite() { 1623 | isPasswordManagerSite = window.location.hostname.includes('www.deno.dev') || 1624 | window.location.hostname.includes('localhost') || 1625 | window.location.hostname.includes('127.0.0.1'); 1626 | return isPasswordManagerSite; 1627 | } 1628 | 1629 | // 高级用户名字段查找 - 完全重写 1630 | function findUsernameFieldsAdvanced() { 1631 | const fields = new Set(); 1632 | 1633 | // 1. 直接查找所有可能的input元素 1634 | const allInputs = document.querySelectorAll('input'); 1635 | 1636 | allInputs.forEach(input => { 1637 | // 跳过不可见、禁用或只读的字段 1638 | if (!isElementVisible(input) || input.disabled || input.readOnly) { 1639 | return; 1640 | } 1641 | 1642 | // 跳过明确的密码字段 1643 | if (input.type === 'password') { 1644 | return; 1645 | } 1646 | 1647 | // 跳过不合适的input类型 1648 | if (['hidden', 'submit', 'button', 'reset', 'file', 'image', 'checkbox', 'radio'].includes(input.type)) { 1649 | return; 1650 | } 1651 | 1652 | // 检查是否是用户名字段的各种条件 1653 | const name = (input.name || '').toLowerCase(); 1654 | const id = (input.id || '').toLowerCase(); 1655 | const placeholder = (input.placeholder || '').toLowerCase(); 1656 | const autocomplete = (input.autocomplete || '').toLowerCase(); 1657 | const className = (input.className || '').toLowerCase(); 1658 | 1659 | // 通过name属性判断 1660 | if (name.includes('email') || name.includes('user') || name.includes('login') || 1661 | name.includes('account') || name.includes('username')) { 1662 | fields.add(input); 1663 | debugLog('✅ 通过name属性识别用户名字段:', input); 1664 | return; 1665 | } 1666 | 1667 | // 通过id属性判断 1668 | if (id.includes('email') || id.includes('user') || id.includes('login') || 1669 | id.includes('account') || id.includes('username')) { 1670 | fields.add(input); 1671 | debugLog('✅ 通过id属性识别用户名字段:', input); 1672 | return; 1673 | } 1674 | 1675 | // 通过placeholder判断 1676 | if (placeholder.includes('email') || placeholder.includes('user') || placeholder.includes('邮箱') || 1677 | placeholder.includes('用户') || placeholder.includes('账号') || placeholder.includes('手机')) { 1678 | fields.add(input); 1679 | debugLog('✅ 通过placeholder识别用户名字段:', input); 1680 | return; 1681 | } 1682 | 1683 | // 通过autocomplete判断 1684 | if (autocomplete.includes('email') || autocomplete.includes('username') || autocomplete.includes('tel')) { 1685 | fields.add(input); 1686 | debugLog('✅ 通过autocomplete识别用户名字段:', input); 1687 | return; 1688 | } 1689 | 1690 | // 通过input类型判断 1691 | if (input.type === 'email' || input.type === 'tel') { 1692 | fields.add(input); 1693 | debugLog('✅ 通过type属性识别用户名字段:', input); 1694 | return; 1695 | } 1696 | 1697 | // Material-UI特殊处理 1698 | if (className.includes('muiinputbase-input') || className.includes('MuiInputBase-input')) { 1699 | // 查找关联的label 1700 | const formControl = input.closest('.MuiFormControl-root'); 1701 | if (formControl) { 1702 | const label = formControl.querySelector('.MuiFormLabel-root, .MuiInputLabel-root'); 1703 | if (label) { 1704 | const labelText = label.textContent.toLowerCase(); 1705 | if (labelText.includes('email') || labelText.includes('user') || labelText.includes('邮箱') || 1706 | labelText.includes('用户') || labelText.includes('账号')) { 1707 | fields.add(input); 1708 | debugLog('✅ 通过Material-UI label识别用户名字段:', input); 1709 | return; 1710 | } 1711 | } 1712 | } 1713 | } 1714 | }); 1715 | 1716 | // 2. 如果没有找到明确的用户名字段,查找第一个text类型的input(在密码字段之前) 1717 | if (fields.size === 0) { 1718 | const passwordField = document.querySelector('input[type="password"]'); 1719 | if (passwordField) { 1720 | const allTextInputs = Array.from(document.querySelectorAll('input[type="text"], input:not([type]), input[type=""]')) 1721 | .filter(input => isElementVisible(input) && !input.disabled && !input.readOnly); 1722 | 1723 | for (const textInput of allTextInputs) { 1724 | // 检查这个text input是否在密码字段之前(在DOM中的位置) 1725 | const comparison = textInput.compareDocumentPosition(passwordField); 1726 | if (comparison & Node.DOCUMENT_POSITION_FOLLOWING) { 1727 | fields.add(textInput); 1728 | debugLog('✅ 通过位置推断识别用户名字段:', textInput); 1729 | break; // 只取第一个 1730 | } 1731 | } 1732 | } 1733 | } 1734 | 1735 | debugLog('🔍 最终找到的用户名字段:', Array.from(fields)); 1736 | return Array.from(fields); 1737 | } 1738 | 1739 | // 高级密码字段查找 - 完全重写 1740 | function findPasswordFieldsAdvanced() { 1741 | const fields = []; 1742 | 1743 | // 查找所有密码字段 1744 | const passwordInputs = document.querySelectorAll('input[type="password"]'); 1745 | 1746 | passwordInputs.forEach(input => { 1747 | if (isElementVisible(input) && !input.disabled && !input.readOnly) { 1748 | fields.push(input); 1749 | debugLog('✅ 找到密码字段:', input); 1750 | } 1751 | }); 1752 | 1753 | debugLog('🔍 最终找到的密码字段:', fields); 1754 | return fields; 1755 | } 1756 | 1757 | // 高级字段填充函数 - 完全重写,专门针对Material-UI 1758 | function fillInputFieldAdvanced(field, value, fieldType) { 1759 | return new Promise(async (resolve) => { 1760 | if (!field || !value) { 1761 | debugLog(`❌ ${fieldType}字段或值为空`); 1762 | resolve(false); 1763 | return; 1764 | } 1765 | 1766 | try { 1767 | debugLog(`🔐 开始填充${fieldType}字段:`, field, '值:', value.substring(0, 3) + '***'); 1768 | 1769 | // 检查字段状态 1770 | if (!isElementVisible(field)) { 1771 | debugLog(`❌ ${fieldType}字段不可见`); 1772 | resolve(false); 1773 | return; 1774 | } 1775 | 1776 | if (field.disabled || field.readOnly) { 1777 | debugLog(`❌ ${fieldType}字段被禁用或只读`); 1778 | resolve(false); 1779 | return; 1780 | } 1781 | 1782 | // 记录原始值 1783 | const originalValue = field.value; 1784 | debugLog(`📝 ${fieldType}字段原始值:`, originalValue); 1785 | 1786 | // 第一步:聚焦并准备字段 1787 | field.focus(); 1788 | debugLog(`👆 ${fieldType}字段已聚焦`); 1789 | 1790 | // 等待聚焦生效 1791 | await new Promise(resolve => setTimeout(resolve, 50)); 1792 | 1793 | // 第二步:React特殊处理 - 在设置值之前 1794 | let reactProps = null; 1795 | try { 1796 | // 查找React实例 1797 | const reactKeys = Object.keys(field).find(key => 1798 | key.startsWith('__reactInternalInstance') || 1799 | key.startsWith('_reactInternalInstance') || 1800 | key.startsWith('__reactInternalFiber') || 1801 | key.startsWith('_reactInternalFiber') 1802 | ); 1803 | 1804 | if (reactKeys) { 1805 | const reactInstance = field[reactKeys]; 1806 | if (reactInstance) { 1807 | reactProps = reactInstance.memoizedProps || 1808 | (reactInstance._currentElement && reactInstance._currentElement.props) || 1809 | reactInstance.return?.memoizedProps; 1810 | debugLog('🔍 找到React实例和props:', reactProps); 1811 | } 1812 | } 1813 | } catch (e) { 1814 | debugLog('⚠️ React实例查找失败:', e); 1815 | } 1816 | 1817 | // 第三步:清空字段 1818 | field.value = ''; 1819 | 1820 | // 触发清空事件 1821 | triggerEventAdvanced(field, 'input', ''); 1822 | 1823 | // 等待清空生效 1824 | await new Promise(resolve => setTimeout(resolve, 50)); 1825 | 1826 | // 第四步:设置新值 - 多种方式同时进行 1827 | 1828 | // 方式1: 直接设置value 1829 | field.value = value; 1830 | debugLog(`📝 方式1完成,当前值:`, field.value); 1831 | 1832 | // 方式2: 使用原生setter 1833 | try { 1834 | const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); 1835 | if (descriptor && descriptor.set) { 1836 | descriptor.set.call(field, value); 1837 | debugLog(`📝 方式2完成,当前值:`, field.value); 1838 | } 1839 | } catch (e) { 1840 | debugLog(`⚠️ 方式2失败:`, e); 1841 | } 1842 | 1843 | // 方式3: React特殊处理 1844 | if (reactProps) { 1845 | try { 1846 | // 清除React的_valueTracker 1847 | if (field._valueTracker) { 1848 | field._valueTracker.setValue(''); 1849 | } 1850 | 1851 | // 直接修改React的内部状态 1852 | const lastValue = field.value; 1853 | field.value = value; 1854 | 1855 | // 创建合成事件 1856 | const event = { 1857 | target: field, 1858 | currentTarget: field, 1859 | type: 'change', 1860 | bubbles: true, 1861 | cancelable: true, 1862 | nativeEvent: new Event('change', { bubbles: true }) 1863 | }; 1864 | 1865 | // 触发React的onChange 1866 | if (reactProps.onChange) { 1867 | reactProps.onChange(event); 1868 | debugLog('✅ React onChange已触发'); 1869 | } 1870 | 1871 | // 触发React的onInput 1872 | if (reactProps.onInput) { 1873 | reactProps.onInput(event); 1874 | debugLog('✅ React onInput已触发'); 1875 | } 1876 | 1877 | debugLog(`📝 React方式完成,当前值:`, field.value); 1878 | } catch (e) { 1879 | debugLog('⚠️ React特殊处理失败:', e); 1880 | } 1881 | } 1882 | 1883 | // 等待React处理 1884 | await new Promise(resolve => setTimeout(resolve, 100)); 1885 | 1886 | // 第五步:Material-UI特殊处理 1887 | try { 1888 | const formControl = field.closest('.MuiFormControl-root'); 1889 | if (formControl) { 1890 | debugLog('🔍 检测到Material-UI表单控件'); 1891 | 1892 | const label = formControl.querySelector('.MuiInputLabel-root, .MuiFormLabel-root'); 1893 | if (label) { 1894 | // 激活label的shrink状态 1895 | label.setAttribute('data-shrink', 'true'); 1896 | label.classList.add('MuiInputLabel-shrink'); 1897 | label.classList.remove('MuiInputLabel-outlined'); 1898 | debugLog('✅ Material-UI label状态已更新'); 1899 | } 1900 | 1901 | // 更新输入框的状态 1902 | const inputBase = formControl.querySelector('.MuiInputBase-root'); 1903 | if (inputBase) { 1904 | inputBase.classList.add('Mui-focused'); 1905 | debugLog('✅ Material-UI输入框focused状态已更新'); 1906 | } 1907 | } 1908 | } catch (e) { 1909 | debugLog('⚠️ Material-UI特殊处理失败:', e); 1910 | } 1911 | 1912 | // 第六步:触发所有相关事件 1913 | triggerEventAdvanced(field, 'input', value); 1914 | triggerEventAdvanced(field, 'change', value); 1915 | 1916 | // 等待事件处理 1917 | await new Promise(resolve => setTimeout(resolve, 100)); 1918 | 1919 | // 第七步:强制保持值 1920 | const checkAndMaintainValue = () => { 1921 | if (field.value !== value) { 1922 | debugLog(`🔧 检测到值被清空,重新设置: ${field.value} -> ${value}`); 1923 | field.value = value; 1924 | 1925 | // 重新触发React事件 1926 | if (reactProps && reactProps.onChange) { 1927 | const event = { 1928 | target: field, 1929 | currentTarget: field, 1930 | type: 'change' 1931 | }; 1932 | reactProps.onChange(event); 1933 | } 1934 | } 1935 | }; 1936 | 1937 | // 多次检查和维护值 1938 | setTimeout(checkAndMaintainValue, 50); 1939 | setTimeout(checkAndMaintainValue, 150); 1940 | setTimeout(checkAndMaintainValue, 300); 1941 | 1942 | // 等待最终稳定 1943 | await new Promise(resolve => setTimeout(resolve, 400)); 1944 | 1945 | // 第八步:验证填充结果 1946 | const finalValue = field.value; 1947 | debugLog(`🔍 ${fieldType}字段最终值:`, finalValue); 1948 | 1949 | if (finalValue === value) { 1950 | // 添加视觉反馈 1951 | field.style.backgroundColor = '#dcfce7'; 1952 | field.style.borderColor = '#10b981'; 1953 | field.style.transition = 'all 0.3s ease'; 1954 | 1955 | setTimeout(() => { 1956 | field.style.backgroundColor = ''; 1957 | field.style.borderColor = ''; 1958 | field.style.transition = ''; 1959 | }, 2000); 1960 | 1961 | debugLog(`✅ ${fieldType}字段填充成功!`); 1962 | resolve(true); 1963 | } else { 1964 | debugLog(`❌ ${fieldType}字段填充失败,期望值: ${value},实际值: ${finalValue}`); 1965 | 1966 | // 最后一次尝试 1967 | debugLog('🔧 进行最后一次填充尝试...'); 1968 | field.value = value; 1969 | 1970 | setTimeout(() => { 1971 | const retryValue = field.value; 1972 | debugLog(`🔍 重试后${fieldType}字段值:`, retryValue); 1973 | resolve(retryValue === value); 1974 | }, 100); 1975 | } 1976 | 1977 | } catch (error) { 1978 | console.error(`❌ 填充${fieldType}字段时发生异常:`, error); 1979 | resolve(false); 1980 | } 1981 | }); 1982 | } 1983 | 1984 | // 高级事件触发函数 1985 | function triggerEventAdvanced(element, eventType, value) { 1986 | try { 1987 | let event; 1988 | 1989 | switch (eventType) { 1990 | case 'input': 1991 | event = new InputEvent('input', { 1992 | bubbles: true, 1993 | cancelable: true, 1994 | data: value, 1995 | inputType: 'insertText' 1996 | }); 1997 | break; 1998 | 1999 | case 'change': 2000 | event = new Event('change', { 2001 | bubbles: true, 2002 | cancelable: true 2003 | }); 2004 | break; 2005 | 2006 | case 'focus': 2007 | event = new FocusEvent('focus', { 2008 | bubbles: true, 2009 | cancelable: true 2010 | }); 2011 | break; 2012 | 2013 | case 'blur': 2014 | event = new FocusEvent('blur', { 2015 | bubbles: true, 2016 | cancelable: true 2017 | }); 2018 | break; 2019 | 2020 | case 'keydown': 2021 | case 'keyup': 2022 | event = new KeyboardEvent(eventType, { 2023 | bubbles: true, 2024 | cancelable: true, 2025 | key: 'Tab' 2026 | }); 2027 | break; 2028 | 2029 | default: 2030 | event = new Event(eventType, { 2031 | bubbles: true, 2032 | cancelable: true 2033 | }); 2034 | } 2035 | 2036 | element.dispatchEvent(event); 2037 | debugLog(`✅ ${eventType}事件已触发`); 2038 | 2039 | } catch (e) { 2040 | debugLog(`❌ 触发${eventType}事件失败:`, e); 2041 | } 2042 | } 2043 | 2044 | // 检查元素是否可见 2045 | function isElementVisible(element) { 2046 | if (!element) return false; 2047 | 2048 | try { 2049 | const rect = element.getBoundingClientRect(); 2050 | const style = window.getComputedStyle(element); 2051 | 2052 | return rect.width > 0 && 2053 | rect.height > 0 && 2054 | style.display !== 'none' && 2055 | style.visibility !== 'hidden' && 2056 | style.opacity !== '0' && 2057 | element.offsetParent !== null; 2058 | } catch (e) { 2059 | return false; 2060 | } 2061 | } 2062 | 2063 | // ========== 浮动按钮显示/隐藏控制 ========== 2064 | 2065 | // 显示浮动按钮 2066 | function showFloatingButton() { 2067 | if (!floatingButton) { 2068 | floatingButton = createFloatingButton(); 2069 | } else if (!document.body.contains(floatingButton)) { 2070 | document.body.appendChild(floatingButton); 2071 | } 2072 | floatingButton.style.display = 'flex'; 2073 | } 2074 | 2075 | // 隐藏浮动按钮 2076 | function hideFloatingButton() { 2077 | if (floatingButton && document.body.contains(floatingButton)) { 2078 | floatingButton.style.display = 'none'; 2079 | } 2080 | } 2081 | 2082 | // 更新按钮显示状态 2083 | function updateButtonVisibility() { 2084 | if (isPasswordManagerSite) { 2085 | showFloatingButton(); 2086 | return; 2087 | } 2088 | 2089 | // 只有检测到登录表单时才显示按钮 2090 | if (detectedForms.length > 0) { 2091 | showFloatingButton(); 2092 | } else { 2093 | hideFloatingButton(); 2094 | } 2095 | } 2096 | 2097 | // ========== 样式 ========== 2098 | 2099 | GM_addStyle(` 2100 | .pm-notification { 2101 | position: fixed; 2102 | top: 20px; 2103 | right: 20px; 2104 | background: linear-gradient(135deg, #10b981, #059669); 2105 | color: white; 2106 | padding: 12px 20px; 2107 | border-radius: 12px; 2108 | box-shadow: 0 10px 25px rgba(0,0,0,0.2); 2109 | z-index: 10000; 2110 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 2111 | font-size: 14px; 2112 | font-weight: 600; 2113 | max-width: 350px; 2114 | transform: translateX(400px); 2115 | transition: transform 0.3s ease; 2116 | cursor: pointer; 2117 | } 2118 | 2119 | .pm-notification.show { 2120 | transform: translateX(0); 2121 | } 2122 | 2123 | .pm-notification.error { 2124 | background: linear-gradient(135deg, #ef4444, #dc2626); 2125 | } 2126 | 2127 | .pm-notification.warning { 2128 | background: linear-gradient(135deg, #f59e0b, #d97706); 2129 | } 2130 | 2131 | .pm-notification.info { 2132 | background: linear-gradient(135deg, #3b82f6, #2563eb); 2133 | } 2134 | 2135 | .pm-floating-btn { 2136 | position: fixed; 2137 | bottom: 20px; 2138 | right: 20px; 2139 | min-width: 48px; 2140 | min-height: 48px; 2141 | background: transparent; 2142 | border: none; 2143 | cursor: pointer; 2144 | z-index: 9999; 2145 | transition: all 0.3s ease; 2146 | display: flex; 2147 | align-items: center; 2148 | justify-content: center; 2149 | user-select: none; 2150 | animation: breathe 4s ease-in-out infinite; 2151 | touch-action: none; 2152 | padding: 0; 2153 | margin: 0; 2154 | border-radius: 50%; 2155 | } 2156 | 2157 | .pm-floating-btn:hover { 2158 | animation-play-state: paused; 2159 | transform: scale(1.1); 2160 | filter: brightness(1.2) drop-shadow(0 8px 16px rgba(0,0,0,0.3)); 2161 | } 2162 | 2163 | .pm-floating-btn.dragging { 2164 | animation-play-state: paused; 2165 | transform: scale(1.1); 2166 | cursor: grabbing; 2167 | filter: brightness(1.3) drop-shadow(0 12px 24px rgba(0,0,0,0.4)); 2168 | } 2169 | 2170 | .pm-floating-btn.has-matches { 2171 | animation: breatheMatched 3.5s ease-in-out infinite; 2172 | } 2173 | 2174 | .pm-floating-btn.multiple-matches { 2175 | animation: breatheMultiple 3s ease-in-out infinite; 2176 | } 2177 | 2178 | .pm-floating-btn .match-count { 2179 | position: absolute; 2180 | top: -8px; 2181 | right: -8px; 2182 | background: #ef4444; 2183 | color: white; 2184 | border-radius: 50%; 2185 | width: 22px; 2186 | height: 22px; 2187 | font-size: 12px; 2188 | font-weight: bold; 2189 | display: flex; 2190 | align-items: center; 2191 | justify-content: center; 2192 | border: 2px solid white; 2193 | box-shadow: 0 2px 8px rgba(0,0,0,0.3); 2194 | animation: pulse 2s ease-in-out infinite; 2195 | } 2196 | 2197 | .pm-floating-btn-icon { 2198 | width: 48px; 2199 | height: 48px; 2200 | object-fit: contain; 2201 | pointer-events: none; 2202 | display: block; 2203 | image-rendering: -webkit-optimize-contrast; 2204 | image-rendering: crisp-edges; 2205 | border-radius: 50%; 2206 | } 2207 | 2208 | .pm-floating-btn.fallback-icon { 2209 | width: 48px; 2210 | height: 48px; 2211 | background: linear-gradient(135deg, #6366f1, #4f46e5); 2212 | color: white; 2213 | font-size: 24px; 2214 | border-radius: 50%; 2215 | display: flex; 2216 | align-items: center; 2217 | justify-content: center; 2218 | } 2219 | 2220 | @keyframes breathe { 2221 | 0%, 100% { 2222 | transform: scale(1); 2223 | filter: brightness(1) drop-shadow(0 4px 8px rgba(0,0,0,0.2)); 2224 | } 2225 | 25% { 2226 | transform: scale(1.03); 2227 | filter: brightness(1.05) drop-shadow(0 6px 12px rgba(0,0,0,0.25)); 2228 | } 2229 | 50% { 2230 | transform: scale(1.08); 2231 | filter: brightness(1.1) drop-shadow(0 8px 16px rgba(0,0,0,0.3)); 2232 | } 2233 | 75% { 2234 | transform: scale(1.05); 2235 | filter: brightness(1.07) drop-shadow(0 7px 14px rgba(0,0,0,0.27)); 2236 | } 2237 | } 2238 | 2239 | @keyframes breatheMatched { 2240 | 0%, 100% { 2241 | transform: scale(1); 2242 | filter: brightness(1) hue-rotate(0deg) drop-shadow(0 4px 8px rgba(16, 185, 129, 0.3)); 2243 | } 2244 | 25% { 2245 | transform: scale(1.04); 2246 | filter: brightness(1.05) hue-rotate(5deg) drop-shadow(0 6px 12px rgba(16, 185, 129, 0.4)); 2247 | } 2248 | 50% { 2249 | transform: scale(1.1); 2250 | filter: brightness(1.15) hue-rotate(10deg) drop-shadow(0 8px 16px rgba(16, 185, 129, 0.5)); 2251 | } 2252 | 75% { 2253 | transform: scale(1.06); 2254 | filter: brightness(1.08) hue-rotate(7deg) drop-shadow(0 7px 14px rgba(16, 185, 129, 0.45)); 2255 | } 2256 | } 2257 | 2258 | @keyframes breatheMultiple { 2259 | 0%, 100% { 2260 | transform: scale(1); 2261 | filter: brightness(1) hue-rotate(0deg) drop-shadow(0 4px 8px rgba(245, 158, 11, 0.3)); 2262 | } 2263 | 20% { 2264 | transform: scale(1.05); 2265 | filter: brightness(1.1) hue-rotate(-5deg) drop-shadow(0 6px 12px rgba(245, 158, 11, 0.4)); 2266 | } 2267 | 40% { 2268 | transform: scale(1.12); 2269 | filter: brightness(1.2) hue-rotate(-10deg) drop-shadow(0 8px 16px rgba(245, 158, 11, 0.5)); 2270 | } 2271 | 60% { 2272 | transform: scale(1.08); 2273 | filter: brightness(1.15) hue-rotate(-7deg) drop-shadow(0 7px 14px rgba(245, 158, 11, 0.45)); 2274 | } 2275 | 80% { 2276 | transform: scale(1.03); 2277 | filter: brightness(1.05) hue-rotate(-3deg) drop-shadow(0 5px 10px rgba(245, 158, 11, 0.35)); 2278 | } 2279 | } 2280 | 2281 | @keyframes pulse { 2282 | 0%, 100% { 2283 | transform: scale(1); 2284 | opacity: 1; 2285 | } 2286 | 50% { 2287 | transform: scale(1.1); 2288 | opacity: 0.8; 2289 | } 2290 | } 2291 | 2292 | .pm-popup { 2293 | position: fixed; 2294 | bottom: 90px; 2295 | right: 20px; 2296 | width: 420px; 2297 | background: white; 2298 | border-radius: 16px; 2299 | box-shadow: 0 20px 40px rgba(0,0,0,0.15); 2300 | z-index: 10000; 2301 | opacity: 0; 2302 | transform: translateY(20px); 2303 | transition: all 0.3s ease; 2304 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 2305 | border: 1px solid rgba(0,0,0,0.1); 2306 | max-height: 600px; 2307 | overflow: hidden; 2308 | display: flex; 2309 | flex-direction: column; 2310 | } 2311 | 2312 | .pm-popup.show { 2313 | opacity: 1; 2314 | transform: translateY(0); 2315 | } 2316 | 2317 | .pm-popup-header { 2318 | padding: 16px 20px; 2319 | border-bottom: 1px solid #e5e7eb; 2320 | display: flex; 2321 | align-items: center; 2322 | justify-content: space-between; 2323 | background: linear-gradient(135deg, #6366f1, #8b5cf6); 2324 | color: white; 2325 | border-radius: 16px 16px 0 0; 2326 | font-weight: 600; 2327 | flex-shrink: 0; 2328 | } 2329 | 2330 | .pm-popup-title { 2331 | display: flex; 2332 | align-items: center; 2333 | gap: 8px; 2334 | } 2335 | 2336 | .pm-match-stats { 2337 | font-size: 12px; 2338 | opacity: 0.9; 2339 | display: flex; 2340 | gap: 8px; 2341 | } 2342 | 2343 | .pm-match-stat { 2344 | display: flex; 2345 | align-items: center; 2346 | gap: 4px; 2347 | } 2348 | 2349 | .pm-match-stat .count { 2350 | background: rgba(255,255,255,0.2); 2351 | padding: 2px 6px; 2352 | border-radius: 10px; 2353 | font-weight: bold; 2354 | } 2355 | 2356 | .pm-popup-content { 2357 | padding: 16px 20px; 2358 | overflow-y: auto; 2359 | flex: 1; 2360 | } 2361 | 2362 | .pm-password-item { 2363 | padding: 16px; 2364 | border: 1px solid #e5e7eb; 2365 | border-radius: 12px; 2366 | margin-bottom: 12px; 2367 | cursor: pointer; 2368 | transition: all 0.3s ease; 2369 | position: relative; 2370 | background: white; 2371 | } 2372 | 2373 | .pm-password-item:hover { 2374 | background: #f8fafc; 2375 | border-color: #6366f1; 2376 | transform: translateY(-2px); 2377 | box-shadow: 0 8px 25px rgba(99, 102, 241, 0.15); 2378 | } 2379 | 2380 | .pm-password-item.exact-match { 2381 | border-color: #10b981; 2382 | background: linear-gradient(135deg, #f0fdf4, #dcfce7); 2383 | } 2384 | 2385 | .pm-password-item.subdomain-match { 2386 | border-color: #3b82f6; 2387 | background: linear-gradient(135deg, #eff6ff, #dbeafe); 2388 | } 2389 | 2390 | .pm-password-item.sitename-match { 2391 | border-color: #f59e0b; 2392 | background: linear-gradient(135deg, #fffbeb, #fef3c7); 2393 | } 2394 | 2395 | .pm-password-item-header { 2396 | display: flex; 2397 | justify-content: space-between; 2398 | align-items: flex-start; 2399 | margin-bottom: 12px; 2400 | } 2401 | 2402 | .pm-password-item-title { 2403 | font-weight: 700; 2404 | color: #1f2937; 2405 | margin-bottom: 6px; 2406 | font-size: 16px; 2407 | } 2408 | 2409 | .pm-password-item-username { 2410 | color: #6b7280; 2411 | font-size: 14px; 2412 | display: flex; 2413 | align-items: center; 2414 | gap: 6px; 2415 | font-weight: 500; 2416 | } 2417 | 2418 | .pm-password-item-url { 2419 | color: #3b82f6; 2420 | font-size: 12px; 2421 | margin-top: 6px; 2422 | overflow: hidden; 2423 | text-overflow: ellipsis; 2424 | white-space: nowrap; 2425 | font-weight: 500; 2426 | } 2427 | 2428 | .pm-match-badge { 2429 | font-size: 11px; 2430 | padding: 4px 8px; 2431 | border-radius: 12px; 2432 | font-weight: 700; 2433 | white-space: nowrap; 2434 | display: flex; 2435 | align-items: center; 2436 | gap: 4px; 2437 | } 2438 | 2439 | .pm-match-badge.exact { 2440 | background: #10b981; 2441 | color: white; 2442 | } 2443 | 2444 | .pm-match-badge.subdomain { 2445 | background: #3b82f6; 2446 | color: white; 2447 | } 2448 | 2449 | .pm-match-badge.sitename { 2450 | background: #f59e0b; 2451 | color: white; 2452 | } 2453 | 2454 | .pm-password-item-meta { 2455 | display: flex; 2456 | justify-content: space-between; 2457 | align-items: center; 2458 | margin-top: 12px; 2459 | font-size: 11px; 2460 | color: #9ca3af; 2461 | font-weight: 500; 2462 | } 2463 | 2464 | .pm-password-item-actions { 2465 | display: flex; 2466 | gap: 8px; 2467 | margin-top: 12px; 2468 | } 2469 | 2470 | .pm-btn-fill { 2471 | background: linear-gradient(135deg, #10b981, #059669); 2472 | color: white; 2473 | border: none; 2474 | padding: 10px 20px; 2475 | border-radius: 8px; 2476 | cursor: pointer; 2477 | font-weight: 600; 2478 | font-size: 14px; 2479 | flex: 1; 2480 | display: flex; 2481 | align-items: center; 2482 | justify-content: center; 2483 | gap: 6px; 2484 | transition: all 0.2s ease; 2485 | } 2486 | 2487 | .pm-btn-fill:hover { 2488 | transform: translateY(-1px); 2489 | box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); 2490 | } 2491 | 2492 | .pm-btn-history { 2493 | background: linear-gradient(135deg, #3b82f6, #2563eb); 2494 | color: white; 2495 | border: none; 2496 | padding: 10px 12px; 2497 | border-radius: 8px; 2498 | cursor: pointer; 2499 | font-weight: 600; 2500 | font-size: 14px; 2501 | display: flex; 2502 | align-items: center; 2503 | justify-content: center; 2504 | transition: all 0.2s ease; 2505 | } 2506 | 2507 | .pm-btn-history:hover { 2508 | transform: translateY(-1px); 2509 | box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 2510 | } 2511 | 2512 | .pm-login-prompt { 2513 | text-align: center; 2514 | color: #6b7280; 2515 | } 2516 | 2517 | .pm-login-btn { 2518 | background: linear-gradient(135deg, #6366f1, #4f46e5); 2519 | color: white; 2520 | border: none; 2521 | padding: 8px 16px; 2522 | border-radius: 6px; 2523 | cursor: pointer; 2524 | font-size: 12px; 2525 | font-weight: 600; 2526 | margin-top: 8px; 2527 | } 2528 | 2529 | .pm-input { 2530 | width: 100%; 2531 | padding: 8px 12px; 2532 | border: 1px solid #e5e7eb; 2533 | border-radius: 6px; 2534 | margin-bottom: 8px; 2535 | font-size: 14px; 2536 | } 2537 | 2538 | .pm-btn { 2539 | background: linear-gradient(135deg, #10b981, #059669); 2540 | color: white; 2541 | border: none; 2542 | padding: 8px 16px; 2543 | border-radius: 6px; 2544 | cursor: pointer; 2545 | font-size: 12px; 2546 | font-weight: 600; 2547 | width: 100%; 2548 | } 2549 | 2550 | .pm-btn-sm { 2551 | padding: 6px 12px; 2552 | font-size: 11px; 2553 | } 2554 | 2555 | .pm-btn-secondary { 2556 | background: #6b7280; 2557 | } 2558 | 2559 | .pm-btn-success { 2560 | background: linear-gradient(135deg, #10b981, #059669); 2561 | } 2562 | 2563 | .pm-btn-danger { 2564 | background: linear-gradient(135deg, #ef4444, #dc2626); 2565 | } 2566 | 2567 | .pm-token-display { 2568 | background: #f8fafc; 2569 | border: 1px solid #e5e7eb; 2570 | border-radius: 8px; 2571 | padding: 12px; 2572 | margin: 12px 0; 2573 | font-family: monospace; 2574 | font-size: 12px; 2575 | word-break: break-all; 2576 | cursor: pointer; 2577 | transition: all 0.2s ease; 2578 | } 2579 | 2580 | .pm-token-display:hover { 2581 | background: #f1f5f9; 2582 | border-color: #6366f1; 2583 | } 2584 | 2585 | .pm-no-matches { 2586 | text-align: center; 2587 | color: #6b7280; 2588 | padding: 20px; 2589 | } 2590 | 2591 | .pm-save-form { 2592 | border-top: 1px solid #e5e7eb; 2593 | padding-top: 16px; 2594 | margin-top: 16px; 2595 | } 2596 | 2597 | .pm-match-summary { 2598 | background: linear-gradient(135deg, #f8fafc, #f1f5f9); 2599 | border: 1px solid #e5e7eb; 2600 | border-radius: 10px; 2601 | padding: 12px; 2602 | margin-bottom: 16px; 2603 | font-size: 13px; 2604 | color: #4b5563; 2605 | } 2606 | 2607 | .pm-match-summary-title { 2608 | font-weight: 600; 2609 | margin-bottom: 8px; 2610 | color: #1f2937; 2611 | } 2612 | 2613 | .pm-match-types { 2614 | display: flex; 2615 | gap: 12px; 2616 | flex-wrap: wrap; 2617 | } 2618 | 2619 | .pm-match-type { 2620 | display: flex; 2621 | align-items: center; 2622 | gap: 4px; 2623 | font-size: 12px; 2624 | } 2625 | 2626 | .pm-match-type-icon { 2627 | width: 8px; 2628 | height: 8px; 2629 | border-radius: 50%; 2630 | } 2631 | 2632 | .pm-match-type-icon.exact { 2633 | background: #10b981; 2634 | } 2635 | 2636 | .pm-match-type-icon.subdomain { 2637 | background: #3b82f6; 2638 | } 2639 | 2640 | .pm-match-type-icon.sitename { 2641 | background: #f59e0b; 2642 | } 2643 | 2644 | .pm-password-history-modal { 2645 | position: fixed; 2646 | top: 0; 2647 | left: 0; 2648 | width: 100%; 2649 | height: 100%; 2650 | z-index: 10002; 2651 | } 2652 | 2653 | .pm-modal-overlay { 2654 | position: absolute; 2655 | top: 0; 2656 | left: 0; 2657 | width: 100%; 2658 | height: 100%; 2659 | background: rgba(0, 0, 0, 0.5); 2660 | backdrop-filter: blur(5px); 2661 | display: flex; 2662 | align-items: center; 2663 | justify-content: center; 2664 | } 2665 | 2666 | .pm-modal-content { 2667 | position: relative; 2668 | background: white; 2669 | border-radius: 16px; 2670 | padding: 24px; 2671 | max-width: 500px; 2672 | width: 90%; 2673 | box-shadow: 0 20px 40px rgba(0,0,0,0.2); 2674 | max-height: 80vh; 2675 | overflow-y: auto; 2676 | } 2677 | 2678 | .pm-modal-header { 2679 | display: flex; 2680 | justify-content: space-between; 2681 | align-items: center; 2682 | margin-bottom: 20px; 2683 | padding-bottom: 16px; 2684 | border-bottom: 1px solid #e5e7eb; 2685 | } 2686 | 2687 | .pm-modal-header h3 { 2688 | margin: 0; 2689 | color: #1f2937; 2690 | font-size: 18px; 2691 | font-weight: 700; 2692 | } 2693 | 2694 | .pm-modal-header-actions { 2695 | display: flex; 2696 | gap: 8px; 2697 | align-items: center; 2698 | } 2699 | 2700 | .pm-close-btn { 2701 | background: none; 2702 | border: none; 2703 | font-size: 20px; 2704 | color: #6b7280; 2705 | cursor: pointer; 2706 | padding: 8px; 2707 | border-radius: 50%; 2708 | transition: all 0.2s ease; 2709 | } 2710 | 2711 | .pm-close-btn:hover { 2712 | background: #f3f4f6; 2713 | color: #374151; 2714 | } 2715 | 2716 | .pm-modal-body { 2717 | margin: 0; 2718 | } 2719 | 2720 | .pm-history-item { 2721 | background: #f8fafc; 2722 | border: 1px solid #e5e7eb; 2723 | border-radius: 12px; 2724 | padding: 16px; 2725 | margin-bottom: 12px; 2726 | } 2727 | 2728 | .pm-history-item:last-child { 2729 | margin-bottom: 0; 2730 | } 2731 | 2732 | .pm-history-header { 2733 | display: flex; 2734 | justify-content: space-between; 2735 | align-items: center; 2736 | margin-bottom: 12px; 2737 | flex-wrap: wrap; 2738 | gap: 8px; 2739 | } 2740 | 2741 | .pm-history-date { 2742 | font-size: 14px; 2743 | color: #6b7280; 2744 | font-weight: 600; 2745 | } 2746 | 2747 | .pm-history-actions { 2748 | display: flex; 2749 | gap: 8px; 2750 | flex-wrap: wrap; 2751 | } 2752 | 2753 | .pm-history-password { 2754 | display: flex; 2755 | align-items: center; 2756 | gap: 8px; 2757 | } 2758 | 2759 | .pm-history-password label { 2760 | font-weight: 600; 2761 | font-size: 14px; 2762 | color: #374151; 2763 | min-width: 60px; 2764 | } 2765 | 2766 | .pm-password-value { 2767 | flex: 1; 2768 | padding: 8px 12px; 2769 | background: white; 2770 | border: 1px solid #d1d5db; 2771 | border-radius: 8px; 2772 | font-family: monospace; 2773 | font-size: 14px; 2774 | } 2775 | 2776 | .pm-text-center { 2777 | text-align: center; 2778 | color: #6b7280; 2779 | padding: 40px 20px; 2780 | font-style: italic; 2781 | } 2782 | 2783 | .pm-form-overlay { 2784 | position: absolute; 2785 | border: 3px solid #10b981; 2786 | background: rgba(16, 185, 129, 0.1); 2787 | pointer-events: none; 2788 | z-index: 9998; 2789 | border-radius: 8px; 2790 | animation: highlightForm 3s ease-in-out; 2791 | } 2792 | 2793 | @keyframes highlightForm { 2794 | 0%, 100% { opacity: 0; } 2795 | 50% { opacity: 1; } 2796 | } 2797 | 2798 | @media (max-width: 768px) { 2799 | .pm-popup { 2800 | width: 95%; 2801 | right: 2.5%; 2802 | bottom: 80px; 2803 | } 2804 | 2805 | .pm-modal-content { 2806 | margin: 16px; 2807 | max-height: 90vh; 2808 | } 2809 | 2810 | .pm-modal-header-actions { 2811 | flex-direction: column; 2812 | gap: 4px; 2813 | } 2814 | 2815 | .pm-history-header { 2816 | flex-direction: column; 2817 | align-items: stretch; 2818 | gap: 12px; 2819 | } 2820 | 2821 | .pm-history-actions { 2822 | justify-content: center; 2823 | } 2824 | 2825 | .pm-floating-btn { 2826 | bottom: 15px; 2827 | right: 15px; 2828 | } 2829 | } 2830 | `); 2831 | 2832 | // ========== 主要功能函数 ========== 2833 | 2834 | // 初始化 2835 | async function init() { 2836 | importantLog('🔐 密码管理助手 Pro 已启动(Material-UI完全修复版)'); 2837 | 2838 | checkPasswordManagerSite(); 2839 | 2840 | // 只在有令牌且未验证时进行验证 2841 | if (authToken && !authVerified) { 2842 | await verifyAuth(); 2843 | } 2844 | 2845 | // 初始检测 2846 | detectLoginForms(); 2847 | updateButtonVisibility(); 2848 | 2849 | observeFormChanges(); 2850 | registerMenuCommands(); 2851 | 2852 | if (isPasswordManagerSite) { 2853 | monitorPasswordManagerAuth(); 2854 | } else if (isAuthenticated) { 2855 | // 启动密码字段监听系统 2856 | startPasswordFieldWatching(); 2857 | } 2858 | } 2859 | 2860 | // 验证登录状态 - 优化版本 2861 | async function verifyAuth() { 2862 | if (!authToken || authVerified) { 2863 | return; 2864 | } 2865 | 2866 | try { 2867 | const response = await makeRequest('/api/auth/verify', { 2868 | method: 'GET', 2869 | headers: { 2870 | 'Authorization': 'Bearer ' + authToken 2871 | } 2872 | }); 2873 | 2874 | if (response.authenticated) { 2875 | isAuthenticated = true; 2876 | currentUser = response.user; 2877 | authVerified = true; // 标记已验证 2878 | 2879 | // 只在密码管理器网站上显示连接成功消息 2880 | if (isPasswordManagerSite) { 2881 | showNotification('🔐 密码管理助手已连接', 'success'); 2882 | } else { 2883 | // 启动密码字段监听系统 2884 | startPasswordFieldWatching(); 2885 | } 2886 | } else { 2887 | authToken = ''; 2888 | GM_setValue(CONFIG.STORAGE_KEY, ''); 2889 | isAuthenticated = false; 2890 | authVerified = false; 2891 | } 2892 | } catch (error) { 2893 | console.error('验证失败:', error); 2894 | isAuthenticated = false; 2895 | authVerified = false; 2896 | } 2897 | } 2898 | 2899 | // 创建浮动按钮 2900 | function createFloatingButton() { 2901 | const btn = document.createElement('button'); 2902 | btn.className = 'pm-floating-btn'; 2903 | btn.title = '密码管理助手 Pro'; 2904 | 2905 | // 从存储中恢复位置 2906 | const savedPosition = GM_getValue('pm_button_position', { bottom: 20, right: 20 }); 2907 | btn.style.bottom = savedPosition.bottom + 'px'; 2908 | btn.style.right = savedPosition.right + 'px'; 2909 | 2910 | // 尝试加载图片 2911 | const icon = document.createElement('img'); 2912 | icon.src = 'https://cdn.mevrik.com/uploads/image6848833820236.png'; 2913 | icon.className = 'pm-floating-btn-icon'; 2914 | icon.alt = 'Password Manager'; 2915 | 2916 | // 图片加载成功 2917 | icon.onload = function() { 2918 | btn.appendChild(icon); 2919 | }; 2920 | 2921 | // 图片加载失败,使用备用图标 2922 | icon.onerror = function() { 2923 | btn.classList.add('fallback-icon'); 2924 | btn.innerHTML = '🔐'; 2925 | }; 2926 | 2927 | try { 2928 | btn.appendChild(icon); 2929 | } catch (e) { 2930 | btn.classList.add('fallback-icon'); 2931 | btn.innerHTML = '🔐'; 2932 | } 2933 | 2934 | // 添加拖拽功能 2935 | let isDragging = false; 2936 | let dragOffset = { x: 0, y: 0 }; 2937 | let startTime = 0; 2938 | 2939 | btn.addEventListener('mousedown', handleDragStart); 2940 | document.addEventListener('mousemove', handleDragMove); 2941 | document.addEventListener('mouseup', handleDragEnd); 2942 | 2943 | btn.addEventListener('touchstart', handleTouchStart, { passive: false }); 2944 | document.addEventListener('touchmove', handleTouchMove, { passive: false }); 2945 | document.addEventListener('touchend', handleTouchEnd); 2946 | 2947 | function handleDragStart(e) { 2948 | e.preventDefault(); 2949 | startDrag(e.clientX, e.clientY); 2950 | } 2951 | 2952 | function handleTouchStart(e) { 2953 | e.preventDefault(); 2954 | const touch = e.touches[0]; 2955 | startDrag(touch.clientX, touch.clientY); 2956 | } 2957 | 2958 | function startDrag(clientX, clientY) { 2959 | isDragging = true; 2960 | startTime = Date.now(); 2961 | btn.classList.add('dragging'); 2962 | 2963 | const rect = btn.getBoundingClientRect(); 2964 | dragOffset.x = clientX - rect.left; 2965 | dragOffset.y = clientY - rect.top; 2966 | 2967 | btn.style.pointerEvents = 'none'; 2968 | } 2969 | 2970 | function handleDragMove(e) { 2971 | if (!isDragging) return; 2972 | e.preventDefault(); 2973 | updatePosition(e.clientX, e.clientY); 2974 | } 2975 | 2976 | function handleTouchMove(e) { 2977 | if (!isDragging) return; 2978 | e.preventDefault(); 2979 | const touch = e.touches[0]; 2980 | updatePosition(touch.clientX, touch.clientY); 2981 | } 2982 | 2983 | function updatePosition(clientX, clientY) { 2984 | const newX = clientX - dragOffset.x; 2985 | const newY = clientY - dragOffset.y; 2986 | 2987 | const windowWidth = window.innerWidth; 2988 | const windowHeight = window.innerHeight; 2989 | const btnWidth = btn.offsetWidth; 2990 | const btnHeight = btn.offsetHeight; 2991 | 2992 | const left = Math.max(0, Math.min(newX, windowWidth - btnWidth)); 2993 | const top = Math.max(0, Math.min(newY, windowHeight - btnHeight)); 2994 | 2995 | const bottom = windowHeight - top - btnHeight; 2996 | const right = windowWidth - left - btnWidth; 2997 | 2998 | btn.style.bottom = bottom + 'px'; 2999 | btn.style.right = right + 'px'; 3000 | btn.style.left = 'auto'; 3001 | btn.style.top = 'auto'; 3002 | } 3003 | 3004 | function handleDragEnd(e) { 3005 | if (!isDragging) return; 3006 | endDrag(); 3007 | } 3008 | 3009 | function handleTouchEnd(e) { 3010 | if (!isDragging) return; 3011 | endDrag(); 3012 | } 3013 | 3014 | function endDrag() { 3015 | const dragDuration = Date.now() - startTime; 3016 | 3017 | isDragging = false; 3018 | btn.classList.remove('dragging'); 3019 | 3020 | const bottom = parseInt(btn.style.bottom); 3021 | const right = parseInt(btn.style.right); 3022 | GM_setValue('pm_button_position', { bottom, right }); 3023 | 3024 | setTimeout(() => { 3025 | btn.style.pointerEvents = 'auto'; 3026 | 3027 | if (dragDuration < 200) { 3028 | togglePasswordManager(); 3029 | } 3030 | }, 100); 3031 | } 3032 | 3033 | btn.addEventListener('click', (e) => { 3034 | if (!isDragging) { 3035 | e.stopPropagation(); 3036 | togglePasswordManager(); 3037 | } 3038 | }); 3039 | 3040 | return btn; 3041 | } 3042 | 3043 | // 切换密码管理器界面 3044 | function togglePasswordManager() { 3045 | if (passwordManagerUI) { 3046 | passwordManagerUI.remove(); 3047 | passwordManagerUI = null; 3048 | return; 3049 | } 3050 | 3051 | createPasswordManagerUI(); 3052 | } 3053 | 3054 | // 创建密码管理器界面 3055 | async function createPasswordManagerUI() { 3056 | const popup = document.createElement('div'); 3057 | popup.className = 'pm-popup'; 3058 | 3059 | if (!isAuthenticated) { 3060 | popup.innerHTML = ` 3061 |
3062 |
3063 | 🔐 3064 | 密码管理助手 Pro 3065 |
3066 |
3067 |
3068 |
3069 |

请先登录密码管理器

3070 | 3071 | ${renderTokenInput()} 3072 |
3073 |
3074 | `; 3075 | } else { 3076 | if (isPasswordManagerSite) { 3077 | popup.innerHTML = ` 3078 |
3079 |
3080 | 🔐 3081 | 密码管理助手 Pro 3082 |
3083 |
3084 |
3085 |
3086 |

✅ 已连接到密码管理器

3087 |
3088 |
3089 |

当前登录令牌:

3090 |
3091 | ${authToken.substring(0, 20)}... 3092 |
3093 |
3094 | 3097 | 3100 |
3101 | `; 3102 | } else { 3103 | // 使用缓存的匹配,如果没有则提示用户点击获取 3104 | const matches = cachedMatches; 3105 | 3106 | if (matches.length === 0) { 3107 | popup.innerHTML = ` 3108 |
3109 |
3110 | 🔐 3111 | 密码管理助手 Pro 3112 |
3113 |
3114 |
3115 |
3116 |

🔍 点击下方按钮获取匹配的账户

3117 | 3120 |
3121 | ${renderDetectedForms()} 3122 |
3123 | `; 3124 | } else { 3125 | popup.innerHTML = ` 3126 |
3127 |
3128 | 🔐 3129 | 密码管理助手 Pro 3130 |
3131 | ${renderMatchStats(matches)} 3132 |
3133 |
3134 | ${renderPasswordMatches(matches)} 3135 | ${renderDetectedForms()} 3136 |
3137 | `; 3138 | } 3139 | } 3140 | } 3141 | 3142 | document.body.appendChild(popup); 3143 | passwordManagerUI = popup; 3144 | 3145 | // 使用事件委托来处理所有点击事件 3146 | popup.addEventListener('click', async (e) => { 3147 | const target = e.target; 3148 | const fillButton = target.closest('.pm-btn-fill'); 3149 | const historyButton = target.closest('.pm-btn-history'); 3150 | const loginBtn = target.closest('.pm-login-btn'); 3151 | const tokenDisplay = target.closest('.pm-token-display'); 3152 | const actionButton = target.closest('.pm-btn'); 3153 | 3154 | if (fillButton) { 3155 | e.preventDefault(); 3156 | fillPasswordFromElement(fillButton); 3157 | } else if (historyButton) { 3158 | e.preventDefault(); 3159 | const passwordId = historyButton.getAttribute('data-password-id'); 3160 | if (passwordId) { 3161 | viewPasswordHistory(passwordId); 3162 | } 3163 | } else if (loginBtn) { 3164 | window.open(CONFIG.API_BASE, '_blank'); 3165 | } else if (tokenDisplay) { 3166 | window.pmExtension.copyToken(authToken); 3167 | } else if (actionButton) { 3168 | const action = actionButton.dataset.action; 3169 | if(action === 'refresh-auth') { 3170 | window.pmExtension.refreshAuth(); 3171 | } else if(action === 'set-token') { 3172 | window.pmExtension.setToken(); 3173 | } else if(action === 'highlight-forms') { 3174 | window.pmExtension.highlightForms(); 3175 | } else if(action === 'get-matches') { 3176 | // 获取匹配账户 3177 | const matches = await window.pmExtension.getPasswordMatches(); 3178 | if (matches.length > 0) { 3179 | // 重新创建UI显示匹配结果 3180 | popup.remove(); 3181 | passwordManagerUI = null; 3182 | createPasswordManagerUI(); 3183 | } 3184 | } else if(action === 'toggle-debug') { 3185 | window.pmExtension.toggleDebugMode(); 3186 | // 重新创建UI以更新按钮文本 3187 | popup.remove(); 3188 | passwordManagerUI = null; 3189 | createPasswordManagerUI(); 3190 | } 3191 | } 3192 | }); 3193 | 3194 | setTimeout(() => popup.classList.add('show'), 10); 3195 | 3196 | document.addEventListener('click', function closePopup(e) { 3197 | if (passwordManagerUI && !passwordManagerUI.contains(e.target) && !e.target.closest('.pm-floating-btn')) { 3198 | passwordManagerUI.remove(); 3199 | passwordManagerUI = null; 3200 | document.removeEventListener('click', closePopup); 3201 | } 3202 | }); 3203 | } 3204 | 3205 | // 渲染匹配统计 3206 | function renderMatchStats(matches) { 3207 | const exactCount = matches.filter(m => m.matchType === 'exact').length; 3208 | const subdomainCount = matches.filter(m => m.matchType === 'subdomain').length; 3209 | const sitenameCount = matches.filter(m => m.matchType === 'sitename').length; 3210 | 3211 | return ` 3212 |
3213 |
3214 |
3215 | ${exactCount} 3216 | 精确 3217 |
3218 |
3219 |
3220 | ${subdomainCount} 3221 | 子域 3222 |
3223 |
3224 |
3225 | ${sitenameCount} 3226 | 站名 3227 |
3228 |
3229 | `; 3230 | } 3231 | 3232 | // 渲染令牌输入 3233 | function renderTokenInput() { 3234 | return ` 3235 |
3236 |

或手动输入登录令牌:

3237 | 3238 | 3241 |
3242 | `; 3243 | } 3244 | 3245 | // 获取密码匹配 - 只在用户主动调用时执行 3246 | async function getPasswordMatches() { 3247 | if (!isAuthenticated || isPasswordManagerSite) return []; 3248 | 3249 | try { 3250 | const response = await makeRequest('/api/auto-fill', { 3251 | method: 'POST', 3252 | headers: { 3253 | 'Content-Type': 'application/json', 3254 | 'Authorization': 'Bearer ' + authToken 3255 | }, 3256 | body: JSON.stringify({ 3257 | url: window.location.href 3258 | }) 3259 | }); 3260 | 3261 | return response.matches || []; 3262 | } catch (error) { 3263 | console.error('获取密码匹配失败:', error); 3264 | return []; 3265 | } 3266 | } 3267 | 3268 | // 渲染密码匹配 3269 | function renderPasswordMatches(matches) { 3270 | let content = ''; 3271 | 3272 | content += ` 3273 |
3274 |
🎯 匹配说明
3275 |
3276 |
3277 |
3278 | 精确:域名完全相同 3279 |
3280 |
3281 |
3282 | 子域:子域名匹配 3283 |
3284 |
3285 |
3286 | 站名:网站名称包含 3287 |
3288 |
3289 |
3290 | `; 3291 | 3292 | content += ` 3293 |
3294 |

3295 | 🔐 选择要填充的账户 (${matches.length} 个) 3296 |

3297 |
3298 | `; 3299 | 3300 | content += renderPasswordList(matches); 3301 | return content; 3302 | } 3303 | 3304 | // 渲染密码列表 3305 | function renderPasswordList(matches) { 3306 | return matches.map((match, index) => { 3307 | const matchTypeText = { 3308 | 'exact': '精确匹配', 3309 | 'subdomain': '子域匹配', 3310 | 'sitename': '站名匹配' 3311 | }; 3312 | 3313 | const matchTypeIcon = { 3314 | 'exact': '🎯', 3315 | 'subdomain': '🌐', 3316 | 'sitename': '🏷️' 3317 | }; 3318 | 3319 | const lastUsed = match.updatedAt ? new Date(match.updatedAt).toLocaleDateString() : '未知'; 3320 | const matchDataAttr = escapeHtml(JSON.stringify(match)); 3321 | 3322 | return ` 3323 |
3324 |
3325 |
3326 |
${escapeHtml(match.siteName)}
3327 |
3328 | 👤 3329 | ${escapeHtml(match.username)} 3330 |
3331 |
3332 |
3333 | ${matchTypeIcon[match.matchType]} 3334 | ${matchTypeText[match.matchType] || match.matchType} 3335 |
3336 |
3337 | 3338 | ${match.url ? `
🔗 ${escapeHtml(match.url)}
` : ''} 3339 | 3340 |
3341 | 3344 | 3347 |
3348 | 3349 |
3350 | 最后使用: ${lastUsed} 3351 | 匹配度: ${match.matchScore}% 3352 |
3353 |
3354 | `; 3355 | }).join(''); 3356 | } 3357 | 3358 | // HTML转义函数 3359 | function escapeHtml(text) { 3360 | if (typeof text !== 'string') { 3361 | text = String(text); 3362 | } 3363 | const div = document.createElement('div'); 3364 | div.textContent = text; 3365 | return div.innerHTML; 3366 | } 3367 | 3368 | // 渲染检测到的表单 3369 | function renderDetectedForms() { 3370 | if (detectedForms.length === 0 || isPasswordManagerSite) return ''; 3371 | 3372 | return ` 3373 |
3374 |

📝 检测到 ${detectedForms.length} 个登录表单

3375 |

登录后可自动保存账户信息

3376 | 3377 |
3378 | `; 3379 | } 3380 | 3381 | // 增强的登录表单检测 - 支持Material-UI等现代框架 3382 | function detectLoginForms() { 3383 | detectedForms = []; 3384 | 3385 | // 策略1: 查找包含用户名和密码字段的 form 3386 | const forms = document.querySelectorAll('form'); 3387 | forms.forEach(form => { 3388 | const usernameFields = findUsernameFieldsAdvanced().filter(field => form.contains(field)); 3389 | const passwordFields = findPasswordFieldsAdvanced().filter(field => form.contains(field)); 3390 | 3391 | if (usernameFields.length > 0 && passwordFields.length > 0) { 3392 | detectedForms.push(form); 3393 | if (CONFIG.AUTO_SAVE && !isPasswordManagerSite) { 3394 | form.removeEventListener('submit', handleFormSubmit); 3395 | form.addEventListener('submit', handleFormSubmit); 3396 | } 3397 | debugLog('✅ 检测到登录表单 (Form-based):', form); 3398 | } 3399 | }); 3400 | 3401 | // 策略2: 如果没有找到form,但找到了用户名和密码字段 3402 | if (detectedForms.length === 0) { 3403 | const usernameFields = findUsernameFieldsAdvanced(); 3404 | const passwordFields = findPasswordFieldsAdvanced(); 3405 | 3406 | if (usernameFields.length > 0 && passwordFields.length > 0) { 3407 | // 创建虚拟表单用于检测 3408 | const virtualForm = document.body; 3409 | detectedForms.push(virtualForm); 3410 | debugLog('✅ 检测到登录字段(无form包裹)'); 3411 | } 3412 | } 3413 | 3414 | debugLog(`🔍 最终检测到 ${detectedForms.length} 个登录表单。`); 3415 | updateButtonVisibility(); 3416 | 3417 | // 重新启动密码字段监听 3418 | if (isAuthenticated && !isPasswordManagerSite) { 3419 | cleanupPasswordFieldWatchers(); 3420 | startPasswordFieldWatching(); 3421 | } 3422 | } 3423 | 3424 | // 处理表单提交 - 保留原有逻辑作为备用 3425 | async function handleFormSubmit(e) { 3426 | if (!isAuthenticated || isPasswordManagerSite) return; 3427 | 3428 | const form = e.target; 3429 | 3430 | // 查找所有密码字段 3431 | const passwordFields = findPasswordFieldsAdvanced().filter(field => form.contains(field)); 3432 | const visiblePasswordFields = passwordFields.filter(field => isElementVisible(field)); 3433 | 3434 | if (visiblePasswordFields.length > 1) { 3435 | debugLog('📝 检测到注册/修改密码表单(存在多个密码框),本次提交将不自动保存密码。'); 3436 | return; 3437 | } 3438 | 3439 | // 查找用户名字段 3440 | const usernameFields = findUsernameFieldsAdvanced().filter(field => form.contains(field)); 3441 | const usernameField = usernameFields[0]; 3442 | const passwordField = visiblePasswordFields[0]; 3443 | 3444 | if (usernameField && passwordField && usernameField.value && passwordField.value) { 3445 | const submitData = { 3446 | url: window.location.href, 3447 | username: usernameField.value, 3448 | password: passwordField.value 3449 | }; 3450 | 3451 | lastSubmittedData = submitData; 3452 | 3453 | // 记录登录尝试并启动快速状态检测 3454 | recordLoginAttemptFast(submitData.username, submitData.password); 3455 | } 3456 | } 3457 | 3458 | // 更新浮动按钮 3459 | function updateFloatingButton(matches) { 3460 | if (!floatingButton) return; 3461 | 3462 | floatingButton.classList.remove('has-matches', 'multiple-matches'); 3463 | const existingCount = floatingButton.querySelector('.match-count'); 3464 | if (existingCount) existingCount.remove(); 3465 | 3466 | if (matches.length > 0) { 3467 | if (matches.length === 1) { 3468 | floatingButton.classList.add('has-matches'); 3469 | floatingButton.title = `找到 1 个匹配的账户`; 3470 | } else { 3471 | floatingButton.classList.add('multiple-matches'); 3472 | floatingButton.title = `找到 ${matches.length} 个匹配的账户`; 3473 | 3474 | const countBadge = document.createElement('div'); 3475 | countBadge.className = 'match-count'; 3476 | countBadge.textContent = matches.length > 9 ? '9+' : matches.length; 3477 | floatingButton.appendChild(countBadge); 3478 | } 3479 | } else { 3480 | floatingButton.title = '密码管理助手 Pro'; 3481 | } 3482 | } 3483 | 3484 | // 监听表单变化 3485 | function observeFormChanges() { 3486 | const observer = new MutationObserver((mutations) => { 3487 | let shouldRedetect = false; 3488 | 3489 | mutations.forEach((mutation) => { 3490 | if (mutation.type === 'childList') { 3491 | mutation.addedNodes.forEach((node) => { 3492 | if (node.nodeType === Node.ELEMENT_NODE) { 3493 | if (node.tagName === 'FORM' || 3494 | (node instanceof HTMLElement && ( 3495 | node.querySelector('input[type="password"]') || 3496 | node.querySelector('input[name*="user" i]') || 3497 | node.querySelector('input[id*="user" i]') || 3498 | node.querySelector('.MuiInputBase-input') || 3499 | node.classList.contains('MuiFormControl-root') 3500 | )) 3501 | ) { 3502 | shouldRedetect = true; 3503 | } 3504 | } 3505 | }); 3506 | } 3507 | }); 3508 | 3509 | if (shouldRedetect) { 3510 | clearTimeout(window._pm_detection_timer); 3511 | window._pm_detection_timer = setTimeout(() => { 3512 | detectLoginForms(); 3513 | }, 500); 3514 | } 3515 | }); 3516 | 3517 | observer.observe(document.body, { 3518 | childList: true, 3519 | subtree: true 3520 | }); 3521 | } 3522 | 3523 | // 监听密码管理器的登录状态 3524 | function monitorPasswordManagerAuth() { 3525 | const originalSetItem = localStorage.setItem; 3526 | localStorage.setItem = function(key, value) { 3527 | if (key === 'authToken') { 3528 | if (value && value !== authToken) { 3529 | authToken = value; 3530 | GM_setValue(CONFIG.STORAGE_KEY, value); 3531 | isAuthenticated = true; 3532 | authVerified = true; 3533 | showNotification('🔐 已自动获取登录令牌', 'success'); 3534 | } 3535 | } 3536 | originalSetItem.apply(this, arguments); 3537 | }; 3538 | 3539 | setInterval(() => { 3540 | const newToken = localStorage.getItem('authToken'); 3541 | if (newToken && newToken !== authToken) { 3542 | authToken = newToken; 3543 | GM_setValue(CONFIG.STORAGE_KEY, newToken); 3544 | isAuthenticated = true; 3545 | authVerified = true; 3546 | showNotification('🔐 密码管理器登录状态已同步', 'success'); 3547 | } 3548 | }, 2000); 3549 | } 3550 | 3551 | // 注册菜单命令 3552 | function registerMenuCommands() { 3553 | GM_registerMenuCommand('🔐 打开密码管理器', () => { 3554 | window.open(CONFIG.API_BASE, '_blank'); 3555 | }); 3556 | 3557 | GM_registerMenuCommand('🔄 重新检测表单', () => { 3558 | detectLoginForms(); 3559 | showNotification('🔍 重新检测完成', 'info'); 3560 | }); 3561 | 3562 | GM_registerMenuCommand('📍 重置按钮位置', () => { 3563 | GM_setValue('pm_button_position', { bottom: 20, right: 20 }); 3564 | if (floatingButton) { 3565 | floatingButton.style.bottom = '20px'; 3566 | floatingButton.style.right = '20px'; 3567 | floatingButton.style.left = 'auto'; 3568 | floatingButton.style.top = 'auto'; 3569 | } 3570 | showNotification('📍 按钮位置已重置', 'info'); 3571 | }); 3572 | 3573 | GM_registerMenuCommand('⚙️ 设置令牌', () => { 3574 | const token = prompt('请输入密码管理器的登录令牌(可在密码管理器中获取):'); 3575 | if (token) { 3576 | authToken = token; 3577 | GM_setValue(CONFIG.STORAGE_KEY, token); 3578 | authVerified = false; 3579 | verifyAuth(); 3580 | } 3581 | }); 3582 | 3583 | GM_registerMenuCommand('🚪 退出登录', () => { 3584 | authToken = ''; 3585 | GM_setValue(CONFIG.STORAGE_KEY, ''); 3586 | isAuthenticated = false; 3587 | authVerified = false; 3588 | cachedMatches = []; 3589 | updateFloatingButton([]); 3590 | cleanupPasswordFieldWatchers(); 3591 | showNotification('👋 已退出登录', 'info'); 3592 | }); 3593 | 3594 | GM_registerMenuCommand('👁️ 强制显示/隐藏按钮', () => { 3595 | if (floatingButton && floatingButton.style.display === 'none') { 3596 | showFloatingButton(); 3597 | showNotification('👁️ 按钮已强制显示', 'info'); 3598 | } else { 3599 | hideFloatingButton(); 3600 | showNotification('👁️ 按钮已隐藏', 'info'); 3601 | } 3602 | }); 3603 | 3604 | GM_registerMenuCommand('🧪 测试填充功能', () => { 3605 | const testData = { 3606 | id: 'test', 3607 | username: 'test@example.com', 3608 | password: 'testpassword123' 3609 | }; 3610 | fillPassword(testData); 3611 | }); 3612 | 3613 | GM_registerMenuCommand('🔧 切换调试模式', () => { 3614 | window.pmExtension.toggleDebugMode(); 3615 | }); 3616 | 3617 | GM_registerMenuCommand('🔍 调试信息', () => { 3618 | importantLog('=== 密码管理助手 Pro 调试信息(Material-UI完全修复版)==='); 3619 | importantLog('认证状态:', isAuthenticated); 3620 | importantLog('认证已验证:', authVerified); 3621 | importantLog('当前用户:', currentUser); 3622 | importantLog('检测到的表单:', detectedForms); 3623 | importantLog('缓存的匹配:', cachedMatches); 3624 | importantLog('页面URL:', window.location.href); 3625 | importantLog('最后提交数据:', lastSubmittedData); 3626 | importantLog('配置信息:', CONFIG); 3627 | importantLog('找到的用户名字段:', findUsernameFieldsAdvanced()); 3628 | importantLog('找到的密码字段:', findPasswordFieldsAdvanced()); 3629 | importantLog('密码字段监听器数量:', passwordFieldWatchers.size); 3630 | importantLog('最后检测到的凭据:', lastDetectedCredentials); 3631 | importantLog('待处理的更新:', pendingUpdates); 3632 | importantLog('登录尝试记录:', loginAttempts); 3633 | importantLog('初始页面状态:', initialPageState); 3634 | importantLog('是否正在监控登录:', isMonitoringLogin); 3635 | importantLog('预检查凭据:', preCheckedCredentials); 3636 | importantLog('快速更新队列:', fastUpdateQueue); 3637 | importantLog('密码监听状态:', isPasswordWatchingActive); 3638 | importantLog('调试模式:', CONFIG.DEBUG_MODE); 3639 | 3640 | showNotification('🔍 调试信息已输出到控制台', 'info'); 3641 | }); 3642 | } 3643 | 3644 | // 显示复制成功状态 3645 | function showCopySuccess() { 3646 | const tokenDisplay = document.querySelector('.pm-token-display'); 3647 | if (tokenDisplay) { 3648 | tokenDisplay.style.background = '#10b981'; 3649 | tokenDisplay.style.borderColor = '#10b981'; 3650 | tokenDisplay.style.color = 'white'; 3651 | setTimeout(() => { 3652 | tokenDisplay.style.background = ''; 3653 | tokenDisplay.style.borderColor = ''; 3654 | tokenDisplay.style.color = ''; 3655 | }, 2000); 3656 | } 3657 | } 3658 | 3659 | // 降级复制方案 3660 | function fallbackCopy(text) { 3661 | try { 3662 | const textArea = document.createElement('textarea'); 3663 | textArea.value = text; 3664 | textArea.style.position = 'fixed'; 3665 | textArea.style.left = '-999999px'; 3666 | textArea.style.top = '-999999px'; 3667 | document.body.appendChild(textArea); 3668 | textArea.focus(); 3669 | textArea.select(); 3670 | 3671 | const successful = document.execCommand('copy'); 3672 | document.body.removeChild(textArea); 3673 | 3674 | if (successful) { 3675 | showCopySuccess(); 3676 | showNotification('📋 已复制到剪贴板', 'success'); 3677 | } else { 3678 | throw new Error('Copy command failed'); 3679 | } 3680 | } catch (error) { 3681 | showNotification('📋 复制失败,请手动复制', 'warning'); 3682 | } 3683 | } 3684 | 3685 | // 发送请求 3686 | function makeRequest(url, options = {}) { 3687 | return new Promise((resolve, reject) => { 3688 | GM_xmlhttpRequest({ 3689 | method: options.method || 'GET', 3690 | url: CONFIG.API_BASE + url, 3691 | headers: options.headers || {}, 3692 | data: options.body, 3693 | onload: function(response) { 3694 | try { 3695 | const data = JSON.parse(response.responseText); 3696 | if (response.status >= 200 && response.status < 300) { 3697 | resolve(data); 3698 | } else { 3699 | reject(new Error(data.error || '请求失败')); 3700 | } 3701 | } catch (error) { 3702 | reject(new Error('解析响应失败')); 3703 | } 3704 | }, 3705 | onerror: function(error) { 3706 | reject(new Error('网络请求失败')); 3707 | } 3708 | }); 3709 | }); 3710 | } 3711 | 3712 | // 显示通知 3713 | function showNotification(message, type = 'success') { 3714 | if (!CONFIG.SHOW_NOTIFICATIONS) return; 3715 | 3716 | const notification = document.createElement('div'); 3717 | notification.className = `pm-notification ${type}`; 3718 | notification.textContent = message; 3719 | 3720 | document.body.appendChild(notification); 3721 | 3722 | setTimeout(() => notification.classList.add('show'), 100); 3723 | 3724 | notification.onclick = () => { 3725 | notification.classList.remove('show'); 3726 | setTimeout(() => notification.remove(), 300); 3727 | }; 3728 | 3729 | setTimeout(() => { 3730 | if(document.body.contains(notification)) { 3731 | notification.classList.remove('show'); 3732 | setTimeout(() => { 3733 | if (document.body.contains(notification)) { 3734 | notification.remove() 3735 | } 3736 | }, 300); 3737 | } 3738 | }, 4000); 3739 | } 3740 | 3741 | // 启动 3742 | if (document.readyState === 'loading') { 3743 | document.addEventListener('DOMContentLoaded', init); 3744 | } else { 3745 | init(); 3746 | } 3747 | })(); 3748 | --------------------------------------------------------------------------------