// ==UserScript== // @name Linux.do 快问快答统计 // @namespace http://tampermonkey.net/ // @version 2025-06-21 // @description 统计用户快问快答标签下提出问题的解答情况,评估用户的提问质量 // @author Haleclipse & Claude // @license MIT // @match https://linux.do/* // @icon https://www.google.com/s2/favicons?sz=64&domain=linux.do // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; const CACHE_PREFIX = 'linuxdo_qa_stats_'; const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24小时缓存 const REQUEST_DELAY_MS = 300; // 请求间隔 const MAX_RETRIES_429 = 3; const RETRY_DELAY_429_MS = 5000; // --- 样式 --- const styles = ` .qa-stats-container { margin-bottom: 20px !important; background: white !important; border-radius: 8px !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; padding: 20px !important; border-left: 4px solid #007bff !important; } .qa-stats-header { display: flex !important; align-items: center !important; margin-bottom: 16px !important; } .qa-stats-icon { font-size: 20px !important; margin-right: 8px !important; } .qa-stats-title { font-size: 1.3em !important; font-weight: bold !important; color: #333 !important; } .qa-stats-content { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 20px !important; margin-bottom: 16px !important; } .qa-stats-left { display: flex !important; flex-direction: column !important; gap: 8px !important; } .qa-stats-item { display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 8px 0 !important; font-size: 14px !important; } .qa-stats-label { color: #666 !important; font-weight: 500 !important; } .qa-stats-value { font-weight: bold !important; color: #333 !important; } .qa-stats-value.primary { color: #007bff !important; font-size: 16px !important; } .qa-stats-value.success { color: #28a745 !important; } .qa-stats-value.warning { color: #ffc107 !important; } .qa-stats-right { display: flex !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; } .qa-stats-progress { width: 100px !important; height: 100px !important; border-radius: 50% !important; background: conic-gradient(#28a745 var(--progress), #e9ecef var(--progress)) !important; display: flex !important; align-items: center !important; justify-content: center !important; margin-bottom: 10px !important; position: relative !important; } .qa-stats-progress::before { content: '' !important; position: absolute !important; width: 70px !important; height: 70px !important; background: white !important; border-radius: 50% !important; } .qa-stats-percentage { position: relative !important; z-index: 1 !important; font-size: 18px !important; font-weight: bold !important; color: #333 !important; } .qa-stats-evaluation { margin-top: 12px !important; padding: 12px !important; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; border-radius: 6px !important; text-align: center !important; border-left: 3px solid var(--eval-color) !important; } .qa-stats-evaluation-text { font-size: 14px !important; color: #333 !important; font-weight: 500 !important; } .qa-stats-loading { display: flex !important; justify-content: center !important; align-items: center !important; padding: 30px 20px !important; background: white !important; border-radius: 8px !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; font-size: 16px !important; color: #555 !important; } .qa-stats-loading .spinner { border: 3px solid rgba(0,0,0,0.1) !important; border-left-color: #007bff !important; border-radius: 50% !important; width: 20px !important; height: 20px !important; animation: spin 1s linear infinite !important; margin-right: 10px !important; } @keyframes spin { to { transform: rotate(360deg); } } @media (max-width: 768px) { .qa-stats-content { grid-template-columns: 1fr !important; } .qa-stats-right { margin-top: 16px !important; } } `; // 创建并注入样式 const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement); // --- 辅助函数 --- function showLoadingIndicator(message = "正在加载快问快答统计...") { removeLoadingIndicator(); const indicator = document.createElement('div'); indicator.className = 'qa-stats-loading'; indicator.innerHTML = `
${message}`; return indicator; } function updateLoadingMessage(indicator, message) { if (indicator) { const textElement = indicator.querySelector('.loading-text'); if (textElement) textElement.textContent = message; } } function removeLoadingIndicator() { const existingIndicator = document.querySelector('.qa-stats-loading'); if (existingIndicator) { existingIndicator.remove(); } } // --- 缓存功能 --- function getCachedData(username) { const cacheKey = `${CACHE_PREFIX}${username}`; try { const cached = localStorage.getItem(cacheKey); if (cached) { const { timestamp, data } = JSON.parse(cached); if (Date.now() - timestamp < CACHE_DURATION_MS) { console.log('快问快答统计: 使用缓存数据', username); return data; } console.log('快问快答统计: 缓存已过期', username); } } catch (e) { console.error('快问快答统计: 读取缓存错误', e); localStorage.removeItem(cacheKey); } return null; } function setCachedData(username, data) { const cacheKey = `${CACHE_PREFIX}${username}`; const itemToCache = { timestamp: Date.now(), data: data }; try { localStorage.setItem(cacheKey, JSON.stringify(itemToCache)); console.log('快问快答统计: 数据已缓存', username); } catch (e) { console.error('快问快答统计: 缓存设置错误', e); } } // --- 数据获取 --- async function fetchUserTopics(username, loadingIndicator) { const cachedData = getCachedData(username); if (cachedData) { return cachedData; } const allTopics = []; let page = 0; const maxPages = 50; // 最多获取50页,约1500个主题 while (page < maxPages) { let retries = 0; let success = false; while (retries <= MAX_RETRIES_429 && !success) { try { if (page > 0 || retries > 0) { await new Promise(resolve => setTimeout(resolve, retries > 0 ? RETRY_DELAY_429_MS : REQUEST_DELAY_MS)); } if (loadingIndicator) { updateLoadingMessage(loadingIndicator, `正在获取主题数据... (第${page + 1}页${retries > 0 ? `, 重试${retries}` : ''})`); } const url = page === 0 ? `https://linux.do/topics/created-by/${username}.json` : `https://linux.do/topics/created-by/${username}.json?page=${page}`; const response = await fetch(url); if (response.status === 429) { retries++; console.warn(`快问快答统计: 429错误,重试 ${retries}/${MAX_RETRIES_429}`); if (loadingIndicator) updateLoadingMessage(loadingIndicator, `服务器限流,正在重试 (${retries}/${MAX_RETRIES_429})...`); if (retries > MAX_RETRIES_429) { throw new Error(`超过最大重试次数`); } continue; } if (!response.ok) { throw new Error(`HTTP错误 ${response.status}`); } const data = await response.json(); if (data.topic_list && data.topic_list.topics && data.topic_list.topics.length > 0) { allTopics.push(...data.topic_list.topics); // 检查是否有更多页面 if (!data.topic_list.more_topics_url) { console.log('快问快答统计: 已获取所有主题'); page = maxPages; // 跳出外层循环 } else { page++; } } else { console.log('快问快答统计: 没有更多主题'); page = maxPages; // 跳出外层循环 } success = true; } catch (error) { console.error('快问快答统计: 获取数据错误', error); if (retries >= MAX_RETRIES_429 || !error.message.includes("429")) { if (loadingIndicator) updateLoadingMessage(loadingIndicator, `获取数据出错,显示已有结果`); await new Promise(resolve => setTimeout(resolve, 2000)); page = maxPages; // 跳出外层循环 break; } retries++; } } if (!success && retries > MAX_RETRIES_429) { console.warn("快问快答统计: 达到最大重试次数,使用已获取的数据"); break; } } console.log(`快问快答统计: 共获取 ${allTopics.length} 个主题`); const resultData = { topics: allTopics }; setCachedData(username, resultData); return resultData; } // --- 数据处理 --- function processQAData(data) { const allTopics = data.topics || []; // 筛选快问快答主题(用户提出的问题) const qaTopics = allTopics.filter(topic => topic.tags && topic.tags.includes('快问快答') ); const total = qaTopics.length; const solved = qaTopics.filter(topic => topic.has_accepted_answer === true).length; const unsolved = total - solved; const solvedRate = total > 0 ? (solved / total * 100) : 0; return { total, solved, unsolved, solvedRate: Math.round(solvedRate * 10) / 10, // 保留一位小数 qaTopics // 返回详细数据供调试 }; } // --- UI创建 --- function createQAStatsWidget(stats) { const container = document.createElement('div'); container.className = 'qa-stats-container'; // 评估用户提问质量 let evaluation = ''; let evalColor = '#6c757d'; if (stats.total === 0) { evaluation = '🤔 暂无快问快答提问记录'; evalColor = '#6c757d'; } else if (stats.solvedRate >= 90) { evaluation = '🌟 提问质量极高,问题描述清晰易懂'; evalColor = '#28a745'; } else if (stats.solvedRate >= 75) { evaluation = '👍 善于提问,大部分问题都能得到解答'; evalColor = '#007bff'; } else if (stats.solvedRate >= 50) { evaluation = '💡 提问能力不错,继续提升问题描述'; evalColor = '#ffc107'; } else if (stats.solvedRate >= 25) { evaluation = '📝 建议优化问题描述,提供更多背景信息'; evalColor = '#fd7e14'; } else { evaluation = '🔍 学习如何提出好问题,会更容易得到帮助'; evalColor = '#dc3545'; } container.innerHTML = `
🤔 快问快答统计
提问总数 ${stats.total}
已获解答 ${stats.solved}
待解答 ${stats.unsolved}
${stats.solvedRate}%
解答率
${evaluation}
`; return container; } // --- 页面检测和集成 --- function isUserSummaryPage() { return window.location.pathname.match(/^\/u\/[^/]+\/summary$/); } function cleanupPreviousWidget() { const existingWidget = document.querySelector('.qa-stats-container'); if (existingWidget) { existingWidget.remove(); } removeLoadingIndicator(); } function waitForUserContent(callback) { const targetNode = document.body; const config = { childList: true, subtree: true }; let userContent = document.querySelector('#user-content'); if (userContent) { callback(userContent); return; } console.log('快问快答统计: 等待 #user-content 元素...'); const observer = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { userContent = document.querySelector('#user-content'); if (userContent) { console.log('快问快答统计: 找到 #user-content 元素'); observer.disconnect(); callback(userContent); return; } } } }); observer.observe(targetNode, config); } // --- 主初始化函数 --- async function init() { if (!isUserSummaryPage()) { cleanupPreviousWidget(); return; } const usernameMatch = window.location.pathname.match(/^\/u\/([^/]+)\/summary$/); if (!usernameMatch || !usernameMatch[1]) { console.error('快问快答统计: 无法从URL提取用户名'); return; } const username = usernameMatch[1]; cleanupPreviousWidget(); const loadingIndicator = showLoadingIndicator(`正在加载 ${username} 的快问快答统计...`); waitForUserContent(async (userContent) => { userContent.prepend(loadingIndicator); try { const data = await fetchUserTopics(username, loadingIndicator); if (!data || !data.topics) { throw new Error("获取的数据无效"); } const stats = processQAData(data); const widget = createQAStatsWidget(stats); userContent.prepend(widget); console.log('快问快答统计: 小部件创建成功', stats); } catch (error) { console.error('快问快答统计: 创建小部件错误:', error); if (loadingIndicator) updateLoadingMessage(loadingIndicator, `加载失败: ${error.message}`); const spinner = loadingIndicator.querySelector('.spinner'); if (spinner) spinner.style.display = 'none'; return; } finally { if (loadingIndicator && !loadingIndicator.textContent.toLowerCase().includes("失败") && !loadingIndicator.textContent.toLowerCase().includes("错误")) { removeLoadingIndicator(); } else if (loadingIndicator) { const spinner = loadingIndicator.querySelector('.spinner'); if (spinner) spinner.style.display = 'none'; } } }); } // --- 页面变化监听 --- let lastUrl = location.href; const urlChangeObserver = new MutationObserver(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; console.log('快问快答统计: URL变化,重新初始化'); init(); } }); urlChangeObserver.observe(document, { subtree: true, childList: true }); // 初始化 init(); })();