├── README.md ├── deepwiki.js ├── image ├── deepwiki.png └── linuxdo-level.png └── linuxdo-level.js /README.md: -------------------------------------------------------------------------------- 1 | # 油猴脚本 2 | 3 | ## 脚本列表 4 | 5 | 脚本名字 | 作用 | 效果展示 6 | ---|---|--- 7 | [deepwiki](./deepwiki.js) | 在 GitHub 项目页添加 DeepWiki 跳转按钮,一键打开对应 DeepWiki 页面 | ![](./image/deepwiki.png) 8 | [linux.do level](./linuxdo-level.js) | 进入 linux.do 没有登录注册按钮时,右侧显示等级浮窗,支持0-3级用户 | ![](./image/linuxdo-level.png) 9 | 10 | ## 使用方法 11 | 12 | 1. 安装 [Tampermonkey](https://www.tampermonkey.net/) 浏览器插件。 13 | 2. 新建脚本,将 [xxx.js](./deepwiki.js) 的内容粘贴进去并保存。 14 | 3. 访问指定网页,即可看到效果 15 | 16 | 17 | --- 18 | 如需反馈或贡献,欢迎访问 [GitHub 仓库](https://github.com/quint11/tampermonkey-script.git)。 -------------------------------------------------------------------------------- /deepwiki.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name DeepWikiJump 3 | // @namespace https://github.com/quint11/tampermonkey-script.git 4 | // @version 1.0 5 | // @description Add a button to open the corresponding wiki page on DeepWiki 6 | // @author quintus 7 | // @icon https://www.google.com/s2/favicons?domain=github.com 8 | // @match https://github.com/*/* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | (function() { 13 | 'use strict'; 14 | 15 | // 检查当前URL是否符合GitHub项目页面格式 16 | function isProjectPage() { 17 | const path = window.location.pathname; 18 | const parts = path.split('/').filter(part => part); 19 | 20 | // 检查是否至少有用户名和仓库名两部分 21 | return parts.length >= 2; 22 | } 23 | 24 | function addLinkToCurrentPage() { 25 | // 如果不是项目页面,则不执行 26 | if (!isProjectPage()) { 27 | return; 28 | } 29 | 30 | // 获取指定类名的元素 31 | const targetElement = document.querySelector('.UnderlineNav-body'); 32 | 33 | // 检测GitHub界面版本并选择合适的元素 34 | function detectGitHubUIVersion() { 35 | // 检查是否为新版GitHub界面 36 | return document.querySelector('header[role="banner"]') !== null; 37 | } 38 | 39 | // 根据GitHub界面版本选择不同的选择器 40 | const isNewUI = detectGitHubUIVersion(); 41 | const newUISelectors = [ 42 | '.UnderlineNav-body', 43 | '.UnderlineNav ul', 44 | 'nav[aria-label="Repository"] ul' 45 | ]; 46 | 47 | const oldUISelectors = [ 48 | '.reponav', 49 | 'nav.js-repo-nav ul', 50 | 'ul.pagehead-actions' 51 | ]; 52 | 53 | // 根据界面版本选择合适的选择器列表 54 | const selectorsToTry = isNewUI ? newUISelectors : oldUISelectors; 55 | 56 | // 尝试使用备选选择器 57 | let finalTargetElement = targetElement; 58 | if (!finalTargetElement) { 59 | for (const selector of selectorsToTry) { 60 | const element = document.querySelector(selector); 61 | if (element) { 62 | finalTargetElement = element; 63 | break; 64 | } 65 | } 66 | } 67 | 68 | if (!finalTargetElement) { 69 | return; 70 | } 71 | 72 | // 检查是否已经添加了DeepWiki按钮,避免重复添加 73 | const existingButton = document.querySelector('.deepwiki-button'); 74 | if (existingButton) { 75 | return; 76 | } 77 | 78 | // 获取当前页面路径 79 | const path = window.location.pathname; 80 | const parts = path.split('/').filter(part => part); 81 | 82 | const username = parts[0]; 83 | const repo = parts[1]; 84 | 85 | // 创建按钮元素 86 | const wikiButton = document.createElement('a'); 87 | // 增大按钮尺寸 88 | wikiButton.classList.add('btn', 'btn-lg', 'btn-outline', 'ml-2', 'deepwiki-button'); 89 | // 使用蓝绿渐变背景 90 | wikiButton.style.background = 'linear-gradient(90deg, #2193b0 0%, #6dd5ed 100%)'; 91 | // 改变按钮文字颜色 92 | wikiButton.style.color = 'white'; 93 | // 添加阴影效果 94 | wikiButton.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)'; 95 | wikiButton.href = `https://deepwiki.com/${username}/${repo}`; 96 | wikiButton.target = '_blank'; 97 | wikiButton.title = 'Open Wiki on DeepWiki'; 98 | // 添加 alt 属性 99 | wikiButton.alt = 'Open Wiki on DeepWiki'; 100 | // 添加 cursor 属性 101 | wikiButton.style.cursor = 'pointer'; 102 | // 设置按钮文字 103 | wikiButton.textContent = 'DeepWiki'; 104 | 105 | // 添加悬停动画 106 | wikiButton.style.transition = 'all 0.3s ease'; 107 | wikiButton.addEventListener('mouseover', function() { 108 | this.style.transform = 'scale(1.1)'; 109 | this.style.boxShadow = '0 6px 12px rgba(0, 0, 0, 0.3)'; 110 | }); 111 | wikiButton.addEventListener('mouseout', function() { 112 | this.style.transform = 'scale(1)'; 113 | this.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)'; 114 | }); 115 | 116 | // 创建列表项元素并将按钮添加到其中 117 | const listItem = document.createElement('li'); 118 | listItem.appendChild(wikiButton); 119 | 120 | // 将列表项添加到目标元素的最后 121 | try { 122 | finalTargetElement.appendChild(listItem); 123 | } catch (error) { 124 | // 静默处理错误 125 | } 126 | } 127 | 128 | // 使用MutationObserver监听DOM变化 129 | function setupObserver() { 130 | // 如果不是项目页面,则不设置观察器 131 | if (!isProjectPage()) { 132 | return; 133 | } 134 | 135 | // 创建一个观察器实例 136 | const observer = new MutationObserver(function() { 137 | // 当DOM变化时尝试添加按钮 138 | addLinkToCurrentPage(); 139 | }); 140 | 141 | // 配置观察选项 142 | const config = { childList: true, subtree: true }; 143 | 144 | // 开始观察document.body的变化 145 | observer.observe(document.body, config); 146 | 147 | // 页面加载完成后也尝试添加按钮 148 | addLinkToCurrentPage(); 149 | } 150 | 151 | // 监听URL变化(GitHub是SPA应用,页面跳转不会重新加载页面) 152 | function setupURLChangeListener() { 153 | let lastURL = location.href; 154 | 155 | // 创建一个新的MutationObserver来监听URL变化 156 | const urlObserver = new MutationObserver(() => { 157 | if (location.href !== lastURL) { 158 | lastURL = location.href; 159 | // URL变化后,如果是项目页面则设置观察器 160 | if (isProjectPage()) { 161 | setupObserver(); 162 | } 163 | } 164 | }); 165 | 166 | // 配置观察选项 167 | urlObserver.observe(document, { subtree: true, childList: true }); 168 | } 169 | 170 | // 添加延迟重试机制 171 | function retryAddButton() { 172 | // 延迟1秒后再次尝试添加按钮 173 | setTimeout(() => { 174 | addLinkToCurrentPage(); 175 | 176 | // 如果仍然没有找到按钮,再次延迟尝试 177 | if (!document.querySelector('.deepwiki-button')) { 178 | setTimeout(addLinkToCurrentPage, 2000); 179 | } 180 | }, 1000); 181 | } 182 | 183 | // 页面加载完成后设置观察器 184 | window.addEventListener('load', () => { 185 | setupObserver(); 186 | setupURLChangeListener(); 187 | retryAddButton(); // 添加延迟重试 188 | }); 189 | 190 | // 对于已经加载完成的页面,立即设置观察器 191 | if (document.readyState === 'complete' || document.readyState === 'interactive') { 192 | setupObserver(); 193 | setupURLChangeListener(); 194 | retryAddButton(); // 对于已加载页面也使用延迟重试 195 | } 196 | })(); -------------------------------------------------------------------------------- /image/deepwiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quint11/tampermonkey-script/6c61597095639a7df4ea1a8e2d6bad616e8410d2/image/deepwiki.png -------------------------------------------------------------------------------- /image/linuxdo-level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quint11/tampermonkey-script/6c61597095639a7df4ea1a8e2d6bad616e8410d2/image/linuxdo-level.png -------------------------------------------------------------------------------- /linuxdo-level.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name linux.do 等级监控浮窗 3 | // @namespace http://tampermonkey.net/ 4 | // @version 2.2 5 | // @description 进入 linux.do 没有登录注册按钮时,右侧显示等级浮窗,支持0-3级用户 6 | // @author quintus 7 | // @match https://linux.do/* 8 | // @grant GM_xmlhttpRequest 9 | // @grant GM_addStyle 10 | // @grant GM_setValue 11 | // @grant GM_getValue 12 | // @grant GM_log 13 | // @connect connect.linux.do 14 | // @connect linux.do 15 | // @connect * 16 | // @run-at document-end 17 | // ==/UserScript== 18 | 19 | (function() { 20 | 'use strict'; 21 | 22 | // 存储数据的键名 23 | const STORAGE_KEY = 'linux_do_user_trust_level_data_v3'; 24 | const LAST_CHECK_KEY = 'linux_do_last_check_v3'; 25 | 26 | // 0级和1级用户的升级要求 27 | const LEVEL_REQUIREMENTS = { 28 | 0: { // 0级升1级 29 | topics_entered: 5, 30 | posts_read_count: 30, 31 | time_read: 600 // 10分钟 = 600秒 32 | }, 33 | 1: { // 1级升2级 34 | days_visited: 15, 35 | likes_given: 1, 36 | likes_received: 1, 37 | replies_to_different_topics: 3, // 特殊字段,需要单独获取 38 | topics_entered: 20, 39 | posts_read_count: 100, 40 | time_read: 3600 // 60分钟 = 3600秒 41 | } 42 | }; 43 | 44 | // 直接在页面上添加调试浮窗 45 | const debugDiv = document.createElement('div'); 46 | debugDiv.style.position = 'fixed'; 47 | debugDiv.style.bottom = '10px'; 48 | debugDiv.style.right = '10px'; 49 | debugDiv.style.width = '300px'; 50 | debugDiv.style.maxHeight = '200px'; 51 | debugDiv.style.overflow = 'auto'; 52 | debugDiv.style.background = 'rgba(0,0,0,0.8)'; 53 | debugDiv.style.color = '#0f0'; 54 | debugDiv.style.padding = '10px'; 55 | debugDiv.style.borderRadius = '5px'; 56 | debugDiv.style.zIndex = '10000'; 57 | debugDiv.style.fontFamily = 'monospace'; 58 | debugDiv.style.fontSize = '12px'; 59 | debugDiv.style.display = 'none'; // 默认隐藏 60 | document.body.appendChild(debugDiv); 61 | 62 | // 调试函数 63 | function debugLog(message) { 64 | const time = new Date().toLocaleTimeString(); 65 | console.log(`[Linux.do脚本] ${message}`); 66 | GM_log(`[Linux.do脚本] ${message}`); 67 | 68 | const logLine = document.createElement('div'); 69 | logLine.textContent = `${time}: ${message}`; 70 | debugDiv.appendChild(logLine); 71 | debugDiv.scrollTop = debugDiv.scrollHeight; 72 | } 73 | 74 | // 按Alt+D显示/隐藏调试窗口 75 | document.addEventListener('keydown', function(e) { 76 | if (e.altKey && e.key === 'd') { 77 | debugDiv.style.display = debugDiv.style.display === 'none' ? 'block' : 'none'; 78 | } 79 | }); 80 | 81 | debugLog('脚本开始执行'); 82 | 83 | // 添加全局样式 - 全新设计 84 | GM_addStyle(` 85 | /* 新的悬浮按钮样式 */ 86 | .ld-floating-container { 87 | position: fixed; 88 | top: 50%; 89 | right: 0; 90 | transform: translateY(-50%); 91 | z-index: 9999; 92 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 93 | } 94 | 95 | .ld-floating-btn { 96 | background: white; 97 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 98 | border: 1px solid #e5e7eb; 99 | border-radius: 8px 0 0 8px; 100 | border-right: none; 101 | transition: all 0.3s ease; 102 | cursor: pointer; 103 | width: 48px; 104 | padding: 12px 0; 105 | display: flex; 106 | flex-direction: column; 107 | align-items: center; 108 | gap: 4px; 109 | user-select: none; 110 | } 111 | 112 | .ld-floating-btn:hover { 113 | width: 64px; 114 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); 115 | } 116 | 117 | .ld-btn-icon { 118 | width: 16px; 119 | height: 16px; 120 | color: #6b7280; 121 | } 122 | 123 | .ld-btn-level { 124 | font-size: 12px; 125 | font-weight: bold; 126 | color: #ea580c; 127 | } 128 | 129 | .ld-btn-progress-bar { 130 | width: 32px; 131 | height: 4px; 132 | background: #e5e7eb; 133 | border-radius: 2px; 134 | overflow: hidden; 135 | } 136 | 137 | .ld-btn-progress-fill { 138 | height: 100%; 139 | background: #ea580c; 140 | border-radius: 2px; 141 | transition: width 0.3s ease; 142 | } 143 | 144 | .ld-btn-stats { 145 | font-size: 10px; 146 | color: #6b7280; 147 | } 148 | 149 | .ld-btn-chevron { 150 | width: 12px; 151 | height: 12px; 152 | color: #9ca3af; 153 | opacity: 0; 154 | transition: opacity 0.3s ease; 155 | } 156 | 157 | .ld-floating-btn:hover .ld-btn-chevron { 158 | opacity: 1; 159 | animation: pulse 1s infinite; 160 | } 161 | 162 | @keyframes pulse { 163 | 0%, 100% { opacity: 1; } 164 | 50% { opacity: 0.5; } 165 | } 166 | 167 | /* 弹出窗口样式 */ 168 | .ld-popup { 169 | position: absolute; 170 | top: 50%; 171 | right: 100%; 172 | margin-right: 8px; 173 | width: 384px; 174 | max-height: 80vh; 175 | background: white; 176 | border-radius: 12px; 177 | box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 178 | border: 1px solid #e5e7eb; 179 | opacity: 0; 180 | transform: translate(20px, -50%); 181 | transition: all 0.2s ease; 182 | pointer-events: none; 183 | overflow: hidden; 184 | overflow-y: auto; 185 | } 186 | 187 | .ld-popup.show { 188 | opacity: 1; 189 | transform: translate(0, -50%); 190 | pointer-events: auto; 191 | } 192 | 193 | /* 当弹出窗口可能超出屏幕时的调整 */ 194 | .ld-popup.adjust-top { 195 | top: 10px; 196 | max-height: calc(100vh - 20px); 197 | transform: translate(20px, 0); 198 | } 199 | 200 | .ld-popup.adjust-top.show { 201 | transform: translate(0, 0); 202 | } 203 | 204 | .ld-popup.adjust-bottom { 205 | top: auto; 206 | bottom: 10px; 207 | max-height: calc(100vh - 20px); 208 | transform: translate(20px, 0); 209 | } 210 | 211 | .ld-popup.adjust-bottom.show { 212 | transform: translate(0, 0); 213 | } 214 | 215 | /* Header 样式 */ 216 | .ld-popup-header { 217 | padding: 16px; 218 | border-bottom: 1px solid #f3f4f6; 219 | } 220 | 221 | .ld-header-top { 222 | display: flex; 223 | align-items: center; 224 | justify-content: space-between; 225 | margin-bottom: 8px; 226 | } 227 | 228 | .ld-user-info { 229 | display: flex; 230 | align-items: center; 231 | gap: 8px; 232 | } 233 | 234 | .ld-user-dot { 235 | width: 12px; 236 | height: 12px; 237 | background: #ea580c; 238 | border-radius: 50%; 239 | } 240 | 241 | .ld-user-name { 242 | font-size: 14px; 243 | font-weight: 500; 244 | color: #374151; 245 | } 246 | 247 | .ld-level-badge { 248 | font-size: 12px; 249 | background: #fed7aa; 250 | color: #c2410c; 251 | padding: 4px 8px; 252 | border-radius: 9999px; 253 | } 254 | 255 | .ld-progress-section { 256 | margin-top: 12px; 257 | } 258 | 259 | .ld-progress-header { 260 | display: flex; 261 | justify-content: space-between; 262 | align-items: center; 263 | margin-bottom: 4px; 264 | } 265 | 266 | .ld-progress-label { 267 | font-size: 12px; 268 | color: #6b7280; 269 | } 270 | 271 | .ld-progress-stats { 272 | font-size: 12px; 273 | color: #4b5563; 274 | } 275 | 276 | .ld-progress-bar-container { 277 | width: 100%; 278 | height: 8px; 279 | background: #e5e7eb; 280 | border-radius: 4px; 281 | overflow: hidden; 282 | } 283 | 284 | .ld-progress-bar { 285 | height: 100%; 286 | background: linear-gradient(90deg, #fb923c, #ea580c); 287 | border-radius: 4px; 288 | transition: width 0.3s ease; 289 | } 290 | 291 | /* 快速状态卡片 */ 292 | .ld-status-cards { 293 | padding: 16px; 294 | display: grid; 295 | grid-template-columns: 1fr 1fr; 296 | gap: 12px; 297 | } 298 | 299 | .ld-status-card { 300 | border-radius: 8px; 301 | padding: 8px; 302 | } 303 | 304 | .ld-status-card.failed { 305 | background: #fef2f2; 306 | } 307 | 308 | .ld-status-card.passed { 309 | background: #f0fdf4; 310 | } 311 | 312 | .ld-card-header { 313 | display: flex; 314 | align-items: center; 315 | gap: 4px; 316 | margin-bottom: 4px; 317 | } 318 | 319 | .ld-card-icon { 320 | width: 12px; 321 | height: 12px; 322 | } 323 | 324 | .ld-card-header.failed { 325 | color: #dc2626; 326 | } 327 | 328 | .ld-card-header.passed { 329 | color: #16a34a; 330 | } 331 | 332 | .ld-card-title { 333 | font-size: 12px; 334 | font-weight: 500; 335 | } 336 | 337 | .ld-card-label { 338 | font-size: 12px; 339 | color: #4b5563; 340 | } 341 | 342 | .ld-card-value { 343 | font-size: 14px; 344 | font-weight: 500; 345 | color: #1f2937; 346 | } 347 | 348 | .ld-card-subtitle { 349 | font-size: 12px; 350 | margin-top: 2px; 351 | } 352 | 353 | .ld-card-subtitle.failed { 354 | color: #dc2626; 355 | } 356 | 357 | .ld-card-subtitle.passed { 358 | color: #16a34a; 359 | } 360 | 361 | /* 详细列表 */ 362 | .ld-details-section { 363 | border-top: 1px solid #f3f4f6; 364 | } 365 | 366 | .ld-details-list { 367 | padding: 12px; 368 | max-height: 256px; 369 | overflow-y: auto; 370 | } 371 | 372 | .ld-detail-item { 373 | display: flex; 374 | align-items: center; 375 | justify-content: space-between; 376 | padding: 4px 8px; 377 | border-radius: 4px; 378 | transition: background 0.2s ease; 379 | } 380 | 381 | .ld-detail-item:hover { 382 | background: #f9fafb; 383 | } 384 | 385 | .ld-detail-left { 386 | display: flex; 387 | align-items: center; 388 | gap: 8px; 389 | flex: 1; 390 | min-width: 0; 391 | } 392 | 393 | .ld-detail-icon { 394 | width: 12px; 395 | height: 12px; 396 | color: #9ca3af; 397 | flex-shrink: 0; 398 | } 399 | 400 | .ld-detail-label { 401 | font-size: 12px; 402 | color: #4b5563; 403 | white-space: nowrap; 404 | overflow: hidden; 405 | text-overflow: ellipsis; 406 | } 407 | 408 | .ld-detail-right { 409 | display: flex; 410 | align-items: center; 411 | gap: 12px; 412 | flex-shrink: 0; 413 | } 414 | 415 | .ld-detail-current { 416 | font-size: 12px; 417 | font-weight: 500; 418 | color: #1f2937; 419 | text-align: right; 420 | } 421 | 422 | .ld-detail-target { 423 | font-size: 12px; 424 | color: #9ca3af; 425 | text-align: right; 426 | } 427 | 428 | .ld-detail-status { 429 | width: 12px; 430 | height: 12px; 431 | } 432 | 433 | .ld-detail-status.passed { 434 | color: #16a34a; 435 | } 436 | 437 | .ld-detail-status.failed { 438 | color: #dc2626; 439 | } 440 | 441 | /* Footer */ 442 | .ld-popup-footer { 443 | padding: 12px; 444 | background: #f9fafb; 445 | border-top: 1px solid #f3f4f6; 446 | text-align: center; 447 | } 448 | 449 | .ld-footer-message { 450 | font-size: 12px; 451 | font-weight: 500; 452 | margin-bottom: 4px; 453 | } 454 | 455 | .ld-footer-message.failed { 456 | color: #dc2626; 457 | } 458 | 459 | .ld-footer-message.passed { 460 | color: #16a34a; 461 | } 462 | 463 | .ld-footer-time { 464 | font-size: 12px; 465 | color: #6b7280; 466 | } 467 | 468 | /* 刷新按钮 */ 469 | .ld-reload-btn { 470 | display: block; 471 | width: calc(100% - 24px); 472 | margin: 0 12px 12px; 473 | padding: 8px; 474 | background: #f3f4f6; 475 | color: #374151; 476 | border: none; 477 | border-radius: 6px; 478 | font-weight: 500; 479 | cursor: pointer; 480 | transition: background 0.2s; 481 | font-size: 12px; 482 | } 483 | 484 | .ld-reload-btn:hover { 485 | background: #e5e7eb; 486 | } 487 | 488 | .ld-reload-btn:disabled { 489 | opacity: 0.5; 490 | cursor: not-allowed; 491 | } 492 | 493 | /* 错误状态 */ 494 | .ld-error-container { 495 | padding: 24px; 496 | text-align: center; 497 | color: #6b7280; 498 | } 499 | 500 | .ld-error-icon { 501 | font-size: 24px; 502 | color: #dc2626; 503 | margin-bottom: 12px; 504 | } 505 | 506 | .ld-error-title { 507 | font-weight: 500; 508 | margin-bottom: 8px; 509 | color: #dc2626; 510 | font-size: 14px; 511 | } 512 | 513 | .ld-error-message { 514 | margin-bottom: 16px; 515 | font-size: 12px; 516 | line-height: 1.5; 517 | } 518 | 519 | /* 隐藏的iframe */ 520 | .ld-hidden-iframe { 521 | position: absolute; 522 | width: 0; 523 | height: 0; 524 | border: 0; 525 | visibility: hidden; 526 | } 527 | 528 | /* 响应式调整 */ 529 | @media (max-height: 600px) { 530 | .ld-details-list { 531 | max-height: 200px; 532 | } 533 | } 534 | `); 535 | 536 | // 工具函数:根据XPath查找元素 537 | function getElementByXpath(xpath) { 538 | return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 539 | } 540 | 541 | // 检查是否有注册和登录按钮 542 | const loginBtnXpath = '//*[@id="ember3"]/div[2]/header/div/div/div[3]/span/span'; 543 | const loginBtn = getElementByXpath(loginBtnXpath); 544 | 545 | debugLog('检查登录按钮: ' + (loginBtn ? '存在' : '不存在')); 546 | 547 | if (loginBtn) { 548 | // 有登录注册按钮,不执行后续逻辑 549 | debugLog('已检测到登录按钮,不显示等级浮窗'); 550 | return; 551 | } 552 | 553 | // 尝试从缓存获取数据 554 | const cachedData = GM_getValue(STORAGE_KEY); 555 | const lastCheck = GM_getValue(LAST_CHECK_KEY, 0); 556 | const now = Date.now(); 557 | const oneHourMs = 60 * 60 * 1000; // 一小时的毫秒数 558 | 559 | debugLog(`上次检查时间: ${new Date(lastCheck).toLocaleString()}`); 560 | 561 | // 创建右侧悬浮按钮容器 562 | const container = document.createElement('div'); 563 | container.className = 'ld-floating-container'; 564 | 565 | // 创建悬浮按钮 566 | const btn = document.createElement('div'); 567 | btn.className = 'ld-floating-btn'; 568 | btn.innerHTML = ` 569 | 570 | 571 | 572 |
L?
573 |
574 |
575 |
576 |
0/0
577 | 578 | 579 | 580 | `; 581 | 582 | // 创建浮窗 583 | const popup = document.createElement('div'); 584 | popup.className = 'ld-popup'; 585 | 586 | // 设置默认内容 587 | popup.innerHTML = ` 588 |
589 |
590 | 594 | 升级到等级? 595 |
596 |
597 |
598 | 完成进度 599 | 0/0 600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 | 611 | 612 | 613 | 未达标 614 |
615 |
正在加载...
616 |
-
617 |
618 |
619 |
620 | 621 | 622 | 623 | 已完成 624 |
625 |
其他要求
626 |
0 / 0
627 |
628 |
629 |
630 | `; 631 | 632 | // 添加到容器 633 | container.appendChild(btn); 634 | container.appendChild(popup); 635 | 636 | // 变量用于跟踪悬停状态 637 | let isHovered = false; 638 | let hoverTimeout = null; 639 | 640 | // 智能调整弹出窗口位置的函数 641 | function adjustPopupPosition() { 642 | const containerRect = container.getBoundingClientRect(); 643 | const viewportHeight = window.innerHeight; 644 | 645 | // 移除之前的调整类 646 | popup.classList.remove('adjust-top', 'adjust-bottom'); 647 | 648 | // 强制重新计算布局 649 | popup.offsetHeight; 650 | 651 | // 获取弹出窗口的实际高度 652 | const popupHeight = popup.scrollHeight; 653 | const margin = 20; // 上下边距 654 | 655 | // 计算弹出窗口的理想位置(居中对齐按钮) 656 | const buttonCenterY = containerRect.top + containerRect.height / 2; 657 | const idealTop = buttonCenterY - popupHeight / 2; 658 | const idealBottom = idealTop + popupHeight; 659 | 660 | debugLog(`视口高度: ${viewportHeight}, 弹窗高度: ${popupHeight}, 按钮中心Y: ${buttonCenterY}`); 661 | debugLog(`理想顶部: ${idealTop}, 理想底部: ${idealBottom}`); 662 | 663 | // 检查是否超出屏幕顶部 664 | if (idealTop < margin) { 665 | popup.classList.add('adjust-top'); 666 | debugLog('弹出窗口调整到顶部对齐'); 667 | } 668 | // 检查是否超出屏幕底部 669 | else if (idealBottom > viewportHeight - margin) { 670 | popup.classList.add('adjust-bottom'); 671 | debugLog('弹出窗口调整到底部对齐'); 672 | } 673 | // 否则使用居中对齐(默认) 674 | else { 675 | debugLog('弹出窗口使用居中对齐'); 676 | } 677 | } 678 | 679 | // 鼠标进入容器时 680 | container.addEventListener('mouseenter', () => { 681 | clearTimeout(hoverTimeout); 682 | isHovered = true; 683 | hoverTimeout = setTimeout(() => { 684 | if (isHovered) { 685 | // 调整位置 686 | adjustPopupPosition(); 687 | 688 | // 显示弹出窗口 689 | popup.classList.add('show'); 690 | } 691 | }, 150); // 稍微延迟显示,避免误触 692 | }); 693 | 694 | // 鼠标离开容器时 695 | container.addEventListener('mouseleave', () => { 696 | clearTimeout(hoverTimeout); 697 | isHovered = false; 698 | hoverTimeout = setTimeout(() => { 699 | if (!isHovered) { 700 | popup.classList.remove('show'); 701 | } 702 | }, 100); // 稍微延迟隐藏,允许鼠标在按钮和弹窗间移动 703 | }); 704 | 705 | // 监听窗口大小变化,重新调整位置 706 | window.addEventListener('resize', () => { 707 | if (popup.classList.contains('show')) { 708 | adjustPopupPosition(); 709 | } 710 | }); 711 | 712 | document.body.appendChild(container); 713 | 714 | debugLog('新版按钮和浮窗已添加到页面'); 715 | 716 | // 如果有缓存数据且时间不超过一小时,直接使用缓存 717 | if (cachedData && (now - lastCheck < oneHourMs)) { 718 | debugLog('使用缓存数据'); 719 | updateInfo( 720 | cachedData.username, 721 | cachedData.currentLevel, 722 | cachedData.targetLevel, 723 | cachedData.trustLevelDetails, 724 | new Date(lastCheck), 725 | cachedData.originalHtml || '', 726 | true // isFromCache 727 | ); 728 | } else { 729 | debugLog('缓存过期或不存在,准备安排获取新数据'); 730 | // 延迟后再执行,给页面一点时间稳定 731 | const delay = 3000; // Increased delay to 3 seconds 732 | debugLog(`将在 ${delay / 1000} 秒后尝试获取数据...`); 733 | setTimeout(() => { 734 | debugLog('Timeout结束,准备调用 fetchDataWithGM'); 735 | fetchDataWithGM(); 736 | }, delay); 737 | } 738 | 739 | // 解析信任级别详情 740 | function parseTrustLevelDetails(targetInfoDivElement) { 741 | const details = { 742 | items: [], 743 | summaryText: '', 744 | achievedCount: 0, 745 | totalCount: 0, 746 | targetLevelInSummary: null // 从 "不符合信任级别 X 要求" 中提取 747 | }; 748 | 749 | if (!targetInfoDivElement) { 750 | debugLog('parseTrustLevelDetails: targetInfoDivElement为空'); 751 | return details; 752 | } 753 | 754 | // 解析表格 755 | const table = targetInfoDivElement.querySelector('table'); 756 | if (table) { 757 | const rows = table.querySelectorAll('tbody tr'); 758 | rows.forEach((row, index) => { 759 | if (index === 0) return; // 跳过表头行 760 | 761 | const cells = row.querySelectorAll('td'); 762 | if (cells.length >= 3) { 763 | const label = cells[0].textContent.trim(); 764 | const currentText = cells[1].textContent.trim(); 765 | const requiredText = cells[2].textContent.trim(); 766 | const isMet = cells[1].classList.contains('text-green-500'); 767 | 768 | details.items.push({ 769 | label: label, 770 | current: currentText, 771 | required: requiredText, 772 | isMet: isMet 773 | }); 774 | 775 | if (isMet) { 776 | details.achievedCount++; 777 | } 778 | } 779 | }); 780 | details.totalCount = details.items.length; 781 | } else { 782 | debugLog('parseTrustLevelDetails: 未找到表格'); 783 | } 784 | 785 | // 解析总结文本,例如 "不符合信任级别 3 要求,继续加油。" 786 | const paragraphs = targetInfoDivElement.querySelectorAll('p'); 787 | paragraphs.forEach(p => { 788 | const text = p.textContent.trim(); 789 | if (text.includes('要求') || text.includes('已满足') || text.includes('信任级别')) { 790 | details.summaryText = text; 791 | const levelMatch = text.match(/信任级别\s*(\d+)/); 792 | if (levelMatch) { 793 | details.targetLevelInSummary = levelMatch[1]; 794 | } 795 | } 796 | }); 797 | if (!details.summaryText) { 798 | debugLog('parseTrustLevelDetails: 未找到总结文本段落'); 799 | } 800 | 801 | debugLog(`parseTrustLevelDetails: 解析完成, ${details.achievedCount}/${details.totalCount} 项达标. 总结: ${details.summaryText}. 目标等级从总结文本: ${details.targetLevelInSummary}`); 802 | return details; 803 | } 804 | 805 | // 使用 GM_xmlhttpRequest 获取 connect.linux.do 的信息 806 | function fetchDataWithGM() { 807 | debugLog('进入 fetchDataWithGM 函数,准备发起 GM_xmlhttpRequest'); 808 | try { 809 | GM_xmlhttpRequest({ 810 | method: "GET", 811 | url: "https://connect.linux.do/", 812 | timeout: 15000, // 15秒超时 813 | onload: function(response) { 814 | debugLog(`GM_xmlhttpRequest 成功: status ${response.status}`); 815 | if (response.status === 200) { 816 | const responseText = response.responseText; 817 | debugLog(`GM_xmlhttpRequest 响应状态 200,准备解析HTML。响应体长度: ${responseText.length}`); 818 | 819 | const tempDiv = document.createElement('div'); 820 | tempDiv.innerHTML = responseText; 821 | 822 | // 1. 解析全局用户名和当前等级 (从

) 823 | let globalUsername = '用户'; 824 | let currentLevel = '未知'; 825 | const h1 = tempDiv.querySelector('h1'); 826 | if (h1) { 827 | const h1Text = h1.textContent.trim(); 828 | // 例如: "你好,一剑万生 (YY_WD) 2级用户" 或 "你好, (yy2025) 0级用户" 829 | const welcomeMatch = h1Text.match(/你好,\s*([^(\s]*)\s*\(?([^)]*)\)?\s*(\d+)级用户/i); 830 | if (welcomeMatch) { 831 | // 优先使用括号内的用户名,如果没有则使用前面的 832 | globalUsername = welcomeMatch[2] || welcomeMatch[1] || '用户'; 833 | currentLevel = welcomeMatch[3]; 834 | debugLog(`从

解析: 全局用户名='${globalUsername}', 当前等级='${currentLevel}'`); 835 | } else { 836 | debugLog(`从

解析: 未匹配到欢迎信息格式: "${h1Text}"`); 837 | } 838 | } else { 839 | debugLog('未在响应中找到

标签'); 840 | } 841 | 842 | // 检查用户等级,决定使用哪种数据获取方式 843 | const userLevel = parseInt(currentLevel); 844 | if (userLevel === 0 || userLevel === 1) { 845 | debugLog(`检测到${userLevel}级用户,使用summary.json获取数据`); 846 | fetchLowLevelUserData(globalUsername, userLevel); 847 | } else if (userLevel >= 2) { 848 | debugLog(`检测到${userLevel}级用户,使用connect.linux.do页面数据`); 849 | // 继续原有逻辑处理2级及以上用户 850 | processHighLevelUserData(tempDiv, globalUsername, currentLevel); 851 | } else { 852 | debugLog('无法确定用户等级,显示错误'); 853 | showError('无法确定用户等级,请检查登录状态'); 854 | } 855 | 856 | } else { 857 | debugLog(`请求失败,状态码: ${response.status} - ${response.statusText}`); 858 | handleRequestError(response); 859 | } 860 | }, 861 | onerror: function(error) { 862 | debugLog(`GM_xmlhttpRequest 错误: ${JSON.stringify(error)}`); 863 | showError('网络请求错误,请检查连接和油猴插件权限'); 864 | }, 865 | ontimeout: function() { 866 | debugLog('GM_xmlhttpRequest 超时'); 867 | showError('请求超时,请检查网络连接'); 868 | }, 869 | onabort: function() { 870 | debugLog('GM_xmlhttpRequest 请求被中止 (onabort)'); 871 | showError('请求被中止,可能是网络问题或扩展冲突'); 872 | } 873 | }); 874 | debugLog('GM_xmlhttpRequest 已调用,等待回调'); 875 | } catch (e) { 876 | debugLog(`调用 GM_xmlhttpRequest 时发生同步错误: ${e.message}`); 877 | showError('调用请求时出错,请查看日志'); 878 | } 879 | } 880 | 881 | // 将数据保存到缓存 882 | function saveDataToCache(username, currentLevel, targetLevel, trustLevelDetails, originalHtml) { 883 | debugLog('保存数据到缓存'); 884 | const dataToCache = { 885 | username, 886 | currentLevel, 887 | targetLevel, 888 | trustLevelDetails, 889 | originalHtml, 890 | cacheTimestamp: Date.now() // 添加一个缓存内的时间戳,方便调试 891 | }; 892 | GM_setValue(STORAGE_KEY, dataToCache); 893 | GM_setValue(LAST_CHECK_KEY, Date.now()); 894 | } 895 | 896 | // 更新信息显示 897 | function updateInfo(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache = false) { 898 | debugLog(`更新信息: 用户='${username}', 当前L=${currentLevel}, 目标L=${targetLevel}, 详情获取=${trustLevelDetails && trustLevelDetails.items.length > 0}, 更新时间=${updateTime.toLocaleString()}`); 899 | 900 | // 计算进度 901 | const achievedCount = trustLevelDetails ? trustLevelDetails.achievedCount : 0; 902 | const totalCount = trustLevelDetails ? trustLevelDetails.totalCount : 0; 903 | const progressPercent = totalCount > 0 ? Math.round((achievedCount / totalCount) * 100) : 0; 904 | 905 | // 更新按钮显示 906 | const levelElement = btn.querySelector('.ld-btn-level'); 907 | const progressFill = btn.querySelector('.ld-btn-progress-fill'); 908 | const statsElement = btn.querySelector('.ld-btn-stats'); 909 | 910 | if (levelElement) levelElement.textContent = `L${currentLevel || '?'}`; 911 | if (progressFill) progressFill.style.width = `${progressPercent}%`; 912 | if (statsElement) statsElement.textContent = `${achievedCount}/${totalCount}`; 913 | 914 | // 更新浮窗内容 915 | updatePopupContent(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache); 916 | } 917 | 918 | // 更新浮窗内容 - 适配新UI结构 919 | function updatePopupContent(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache = false) { 920 | // 如果加载失败或无数据,显示错误状态 921 | if (!trustLevelDetails || !trustLevelDetails.items || trustLevelDetails.items.length === 0) { 922 | showPopupError('无法加载数据', '未能获取到信任级别详情数据,请刷新重试。', updateTime); 923 | return; 924 | } 925 | 926 | // 计算进度 927 | const achievedCount = trustLevelDetails.achievedCount; 928 | const totalCount = trustLevelDetails.totalCount; 929 | const progressPercent = Math.round((achievedCount / totalCount) * 100); 930 | 931 | // 找到未达标的项目 932 | const failedItems = trustLevelDetails.items.filter(item => !item.isMet); 933 | const failedItem = failedItems.length > 0 ? failedItems[0] : null; 934 | 935 | // 获取图标函数 936 | function getIconSvg(type) { 937 | const icons = { 938 | user: '', 939 | message: '', 940 | eye: '', 941 | thumbsUp: '', 942 | warning: '', 943 | shield: '' 944 | }; 945 | return icons[type] || icons.user; 946 | } 947 | 948 | function getItemIcon(label) { 949 | if (label.includes('访问次数')) return 'user'; 950 | if (label.includes('回复') || label.includes('话题')) return 'message'; 951 | if (label.includes('浏览') || label.includes('已读')) return 'eye'; 952 | if (label.includes('举报')) return 'warning'; 953 | if (label.includes('点赞') || label.includes('获赞')) return 'thumbsUp'; 954 | if (label.includes('禁言') || label.includes('封禁')) return 'shield'; 955 | return 'user'; 956 | } 957 | 958 | // 构建新UI HTML 959 | let html = ` 960 |
961 |
962 | 966 | 升级到等级${targetLevel} 967 |
968 |
969 |
970 | 完成进度 971 | ${achievedCount}/${totalCount} 972 |
973 |
974 |
975 |
976 |
977 |
978 | 979 |
980 |
981 |
982 | 983 | 984 | 985 | 未达标 986 |
987 |
${failedItem ? failedItem.label : '无'}
988 |
${failedItem ? failedItem.current : '所有要求均已满足'}
989 | ${failedItem ? `
需要 ${failedItem.required}
` : ''} 990 |
991 |
992 |
993 | 994 | 995 | 996 | 已完成 997 |
998 |
其他要求
999 |
${achievedCount} / ${totalCount}
1000 |
1001 |
1002 | 1003 |
1004 |
`; 1005 | 1006 | // 为每个指标生成HTML 1007 | trustLevelDetails.items.forEach(item => { 1008 | const iconType = getItemIcon(item.label); 1009 | html += ` 1010 |
1011 |
1012 | 1013 | ${getIconSvg(iconType)} 1014 | 1015 | ${item.label} 1016 |
1017 |
1018 | ${item.current} 1019 | /${item.required} 1020 | 1021 | ${item.isMet ? 1022 | '' : 1023 | '' 1024 | } 1025 | 1026 |
1027 |
`; 1028 | }); 1029 | 1030 | // 添加底部状态和更新时间 1031 | html += ` 1032 |
1033 |
1034 | 1035 | 1041 | 1042 | `; 1043 | 1044 | // 设置内容 1045 | popup.innerHTML = html; 1046 | 1047 | // 添加事件监听器 1048 | setTimeout(() => { 1049 | // 刷新按钮 1050 | const reloadBtn = popup.querySelector('.ld-reload-btn'); 1051 | if (reloadBtn) { 1052 | reloadBtn.addEventListener('click', function() { 1053 | this.textContent = '加载中...'; 1054 | this.disabled = true; 1055 | fetchDataWithGM(); 1056 | setTimeout(() => { 1057 | if (!this.isConnected) return; // 检查按钮是否还在DOM中 1058 | this.textContent = '刷新数据'; 1059 | this.disabled = false; 1060 | }, 3000); 1061 | }); 1062 | } 1063 | }, 100); 1064 | } 1065 | 1066 | // 显示错误状态的浮窗 1067 | function showPopupError(title, message, updateTime) { 1068 | popup.innerHTML = ` 1069 |
1070 |
1071 |
${title}
1072 |
${message}
1073 | 1074 |
1075 | 1076 | `; 1077 | 1078 | // 添加重试按钮事件 1079 | setTimeout(() => { 1080 | const retryBtn = popup.querySelector('.ld-reload-btn'); 1081 | if (retryBtn) { 1082 | retryBtn.addEventListener('click', function() { 1083 | this.textContent = '加载中...'; 1084 | this.disabled = true; 1085 | fetchDataWithGM(); 1086 | setTimeout(() => { 1087 | if (!this.isConnected) return; 1088 | this.textContent = '重试'; 1089 | this.disabled = false; 1090 | }, 3000); 1091 | }); 1092 | } 1093 | }, 100); 1094 | } 1095 | 1096 | // 显示错误信息 (保留向下兼容) 1097 | function showError(message) { 1098 | debugLog(`显示错误: ${message}`); 1099 | showPopupError('出错了', message, new Date()); 1100 | } 1101 | 1102 | // 处理请求错误 1103 | function handleRequestError(response) { 1104 | let responseBody = response.responseText || ""; 1105 | debugLog(`响应内容 (前500字符): ${responseBody.substring(0, 500)}`); 1106 | 1107 | if (response.status === 429) { 1108 | showError('请求过于频繁 (429),请稍后重试。Cloudflare可能暂时限制了访问。'); 1109 | } else if (responseBody.includes('Cloudflare') || responseBody.includes('challenge-platform') || responseBody.includes('Just a moment')) { 1110 | showError('Cloudflare拦截或验证页面。请等待或手动访问connect.linux.do完成验证。'); 1111 | } else if (responseBody.includes('登录') || responseBody.includes('注册')) { 1112 | showError('获取数据失败,可能是需要登录 connect.linux.do。'); 1113 | } else { 1114 | showError(`获取数据失败 (状态: ${response.status})`); 1115 | } 1116 | } 1117 | 1118 | // 处理2级及以上用户数据(原有逻辑) 1119 | function processHighLevelUserData(tempDiv, globalUsername, currentLevel) { 1120 | let targetInfoDiv = null; 1121 | const potentialDivs = tempDiv.querySelectorAll('div.bg-white.p-6.rounded-lg.mb-4.shadow'); 1122 | debugLog(`找到了 ${potentialDivs.length} 个潜在的 'div.bg-white.p-6.rounded-lg.mb-4.shadow' 元素。`); 1123 | 1124 | for (let i = 0; i < potentialDivs.length; i++) { 1125 | const div = potentialDivs[i]; 1126 | const h2 = div.querySelector('h2.text-xl.mb-4.font-bold'); 1127 | if (h2 && h2.textContent.includes('信任级别')) { 1128 | targetInfoDiv = div; 1129 | debugLog(`找到包含"信任级别"标题的目标div,其innerHTML (前200字符): ${targetInfoDiv.innerHTML.substring(0,200)}`); 1130 | break; 1131 | } 1132 | } 1133 | 1134 | if (!targetInfoDiv) { 1135 | debugLog('通过遍历和内容检查,未找到包含"信任级别"标题的目标div。'); 1136 | showError('未找到包含等级信息的数据块。请检查控制台日志 (Alt+D) 中的HTML内容,并提供一个准确的选择器。'); 1137 | return; 1138 | } 1139 | 1140 | debugLog('通过内容匹配,在响应中找到目标信息div。'); 1141 | const originalHtml = targetInfoDiv.innerHTML; 1142 | 1143 | // 从目标div的

解析用户名和目标等级 1144 | let specificUsername = globalUsername; 1145 | let targetLevel = '未知'; 1146 | const h2InDiv = targetInfoDiv.querySelector('h2.text-xl.mb-4.font-bold'); 1147 | if (h2InDiv) { 1148 | const h2Text = h2InDiv.textContent.trim(); 1149 | const titleMatch = h2Text.match(/^(.+?)\s*-\s*信任级别\s*(\d+)\s*的要求/i); 1150 | if (titleMatch) { 1151 | specificUsername = titleMatch[1].trim(); 1152 | targetLevel = titleMatch[2]; 1153 | debugLog(`从

解析: 特定用户名='${specificUsername}', 目标等级='${targetLevel}'`); 1154 | } else { 1155 | debugLog(`从

解析: 未匹配到标题格式: "${h2Text}"`); 1156 | } 1157 | } else { 1158 | debugLog('目标div中未找到

标签'); 1159 | } 1160 | 1161 | // 解析信任级别详情 1162 | const trustLevelDetails = parseTrustLevelDetails(targetInfoDiv); 1163 | 1164 | debugLog(`最终提取信息: 用户名='${specificUsername}', 当前等级='${currentLevel}', 目标等级='${targetLevel}'`); 1165 | updateInfo(specificUsername, currentLevel, targetLevel, trustLevelDetails, new Date(), originalHtml); 1166 | saveDataToCache(specificUsername, currentLevel, targetLevel, trustLevelDetails, originalHtml); 1167 | } 1168 | 1169 | // 处理0级和1级用户数据 1170 | function fetchLowLevelUserData(username, currentLevel) { 1171 | debugLog(`开始获取${currentLevel}级用户 ${username} 的数据`); 1172 | 1173 | // 首先获取summary.json数据 1174 | GM_xmlhttpRequest({ 1175 | method: "GET", 1176 | url: `https://linux.do/u/${username}/summary.json`, 1177 | timeout: 15000, 1178 | onload: function(response) { 1179 | debugLog(`summary.json请求成功: status ${response.status}`); 1180 | if (response.status === 200) { 1181 | try { 1182 | const data = JSON.parse(response.responseText); 1183 | const userSummary = data.user_summary; 1184 | debugLog(`获取到用户摘要数据: ${JSON.stringify(userSummary)}`); 1185 | 1186 | if (currentLevel === 1) { 1187 | // 1级用户需要额外获取回复数据 1188 | fetchUserRepliesData(username, currentLevel, userSummary); 1189 | } else { 1190 | // 0级用户直接处理数据 1191 | processLowLevelUserData(username, currentLevel, userSummary, null); 1192 | } 1193 | } catch (e) { 1194 | debugLog(`解析summary.json失败: ${e.message}`); 1195 | showError('解析用户数据失败'); 1196 | } 1197 | } else { 1198 | debugLog(`summary.json请求失败: ${response.status}`); 1199 | showError(`获取用户数据失败 (状态: ${response.status})`); 1200 | } 1201 | }, 1202 | onerror: function(error) { 1203 | debugLog(`summary.json请求错误: ${JSON.stringify(error)}`); 1204 | showError('获取用户数据时网络错误'); 1205 | }, 1206 | ontimeout: function() { 1207 | debugLog('summary.json请求超时'); 1208 | showError('获取用户数据超时'); 1209 | } 1210 | }); 1211 | } 1212 | 1213 | // 获取用户回复数据(仅1级用户需要) 1214 | function fetchUserRepliesData(username, currentLevel, userSummary) { 1215 | debugLog(`获取用户 ${username} 的回复数据`); 1216 | 1217 | GM_xmlhttpRequest({ 1218 | method: "GET", 1219 | url: `https://linux.do/u/${username}/activity/replies`, 1220 | timeout: 15000, 1221 | onload: function(response) { 1222 | debugLog(`replies页面请求成功: status ${response.status}`); 1223 | if (response.status === 200) { 1224 | const tempDiv = document.createElement('div'); 1225 | tempDiv.innerHTML = response.responseText; 1226 | 1227 | // 统计回复的不同话题数量 1228 | const replyContainer = tempDiv.querySelector('#main-outlet div:nth-child(3) section div'); 1229 | let repliesCount = 0; 1230 | 1231 | if (replyContainer) { 1232 | const replyItems = replyContainer.querySelectorAll('#user-content > div > div:nth-child(1) > div'); 1233 | repliesCount = Math.min(replyItems.length, 3); // 最多统计3个,满足要求即可 1234 | debugLog(`找到 ${replyItems.length} 个回复项,统计 ${repliesCount} 个`); 1235 | } else { 1236 | debugLog('未找到回复容器'); 1237 | } 1238 | 1239 | processLowLevelUserData(username, currentLevel, userSummary, repliesCount); 1240 | } else { 1241 | debugLog(`replies页面请求失败: ${response.status}`); 1242 | // 即使获取回复数据失败,也继续处理其他数据,回复数设为0 1243 | processLowLevelUserData(username, currentLevel, userSummary, 0); 1244 | } 1245 | }, 1246 | onerror: function(error) { 1247 | debugLog(`replies页面请求错误: ${JSON.stringify(error)}`); 1248 | processLowLevelUserData(username, currentLevel, userSummary, 0); 1249 | }, 1250 | ontimeout: function() { 1251 | debugLog('replies页面请求超时'); 1252 | processLowLevelUserData(username, currentLevel, userSummary, 0); 1253 | } 1254 | }); 1255 | } 1256 | 1257 | // 处理0级和1级用户的数据 1258 | function processLowLevelUserData(username, currentLevel, userSummary, repliesCount) { 1259 | debugLog(`处理${currentLevel}级用户数据: ${username}`); 1260 | 1261 | const targetLevel = currentLevel + 1; // 目标等级 1262 | const requirements = LEVEL_REQUIREMENTS[currentLevel]; 1263 | 1264 | if (!requirements) { 1265 | showError(`未找到等级${currentLevel}的升级要求配置`); 1266 | return; 1267 | } 1268 | 1269 | // 构建升级详情数据 1270 | const trustLevelDetails = { 1271 | items: [], 1272 | summaryText: '', 1273 | achievedCount: 0, 1274 | totalCount: 0, 1275 | targetLevelInSummary: targetLevel.toString() 1276 | }; 1277 | 1278 | // 检查各项要求 1279 | Object.entries(requirements).forEach(([key, requiredValue]) => { 1280 | let currentValue = 0; 1281 | let label = ''; 1282 | let isMet = false; 1283 | 1284 | switch (key) { 1285 | case 'topics_entered': 1286 | currentValue = userSummary.topics_entered || 0; 1287 | label = '浏览的话题'; 1288 | isMet = currentValue >= requiredValue; 1289 | break; 1290 | case 'posts_read_count': 1291 | currentValue = userSummary.posts_read_count || 0; 1292 | label = '已读帖子'; 1293 | isMet = currentValue >= requiredValue; 1294 | break; 1295 | case 'time_read': 1296 | currentValue = Math.floor((userSummary.time_read || 0) / 60); // 转换为分钟 1297 | label = '阅读时间(分钟)'; 1298 | isMet = (userSummary.time_read || 0) >= requiredValue; 1299 | break; 1300 | case 'days_visited': 1301 | currentValue = userSummary.days_visited || 0; 1302 | label = '访问天数'; 1303 | isMet = currentValue >= requiredValue; 1304 | break; 1305 | case 'likes_given': 1306 | currentValue = userSummary.likes_given || 0; 1307 | label = '给出的赞'; 1308 | isMet = currentValue >= requiredValue; 1309 | break; 1310 | case 'likes_received': 1311 | currentValue = userSummary.likes_received || 0; 1312 | label = '收到的赞'; 1313 | isMet = currentValue >= requiredValue; 1314 | break; 1315 | case 'replies_to_different_topics': 1316 | currentValue = repliesCount || 0; 1317 | label = '回复不同话题'; 1318 | isMet = currentValue >= requiredValue; 1319 | break; 1320 | } 1321 | 1322 | if (label) { 1323 | trustLevelDetails.items.push({ 1324 | label: label, 1325 | current: currentValue.toString(), 1326 | required: key === 'time_read' ? Math.floor(requiredValue / 60).toString() : requiredValue.toString(), 1327 | isMet: isMet 1328 | }); 1329 | 1330 | if (isMet) { 1331 | trustLevelDetails.achievedCount++; 1332 | } 1333 | trustLevelDetails.totalCount++; 1334 | } 1335 | }); 1336 | 1337 | // 生成总结文本 1338 | if (trustLevelDetails.achievedCount === trustLevelDetails.totalCount) { 1339 | trustLevelDetails.summaryText = `已满足信任级别 ${targetLevel} 要求`; 1340 | } else { 1341 | trustLevelDetails.summaryText = `不符合信任级别 ${targetLevel} 要求,继续加油`; 1342 | } 1343 | 1344 | debugLog(`${currentLevel}级用户数据处理完成: ${trustLevelDetails.achievedCount}/${trustLevelDetails.totalCount} 项达标`); 1345 | 1346 | // 更新显示 1347 | updateInfo(username, currentLevel.toString(), targetLevel.toString(), trustLevelDetails, new Date(), '', false); 1348 | saveDataToCache(username, currentLevel.toString(), targetLevel.toString(), trustLevelDetails, ''); 1349 | } 1350 | })(); 1351 | --------------------------------------------------------------------------------