GitHub Super Enhancer

GitHub 仓库增强:仓库主页、星标页、探索/搜索/议题等列表页显示仓库大小、星标页作者头像与仓库备注、不活跃仓库警告

Verzia zo dňa 16.08.2026. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu budete musieť nainštalovať rozšírenie, ako je napríklad Tampermonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         GitHub Super Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  GitHub 仓库增强:仓库主页、星标页、探索/搜索/议题等列表页显示仓库大小、星标页作者头像与仓库备注、不活跃仓库警告
// @author       miscellaneouszx
// @match        https://github.com/*
// @icon         https://github.githubassets.com/favicons/favicon.png
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ================= 配置区 =================
    // 未填 Token 时,GitHub API 每小时只有约 60 次请求额度,浏览仓库一多就会触发 403 限流,
    // 标签就会突然不显示。强烈建议生成 Token 填到下面:
    //   1. 打开 https://github.com/settings/tokens
    //   2. 「Generate new token」→「Generate new token (classic)」
    //   3. 读取公开仓库大小不需要勾选任何权限(scope),直接生成即可
    //   4. 把生成的 ghp_... 粘贴到下面两个引号之间
    // 填了 Token 后额度提升到每小时 5000 次,基本不会再触发限流。
    const GITHUB_TOKEN = '';
    const CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 仓库数据缓存 7 天
    const INACTIVE_MONTHS = 6;                            // 超过多少个月没有 push 才提示
    const STAR_SCAN_RETRY = [0, 500, 1500, 3500];       // 星标页异步加载时,补扫几次
    const STAR_SIZE_CONCURRENCY = 4;                       // 星标页同时查询 GitHub API 的数量
    // ==========================================

    GM_addStyle(`
        .custom-header-size {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 4px;
            height: 20px;
            box-sizing: border-box;
            padding: 0 7px;
            margin-left: 6px;
            border: 1px solid var(--borderColor-accent-emphasis, #0969da);
            border-radius: 999px;
            color: var(--fgColor-accent, #0969da);
            background: transparent;
            font-size: 12px;
            font-weight: 500;
            line-height: 18px;
            vertical-align: middle;
            white-space: nowrap;
        }

        .custom-header-size svg {
            width: 14px;
            height: 14px;
            flex: 0 0 auto;
        }

        /* 星标页仓库名后的大小标签:与主页标签保持同样的胶囊风格,但更紧凑 */
        .custom-star-size {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 3px;
            height: 20px;
            box-sizing: border-box;
            padding: 0 7px;
            margin-left: 6px;
            border: 1px solid var(--borderColor-accent-emphasis, #0969da);
            border-radius: 999px;
            color: var(--fgColor-accent, #0969da);
            background: transparent;
            font-size: 12px;
            font-weight: 500;
            line-height: 18px;
            vertical-align: middle;
            white-space: nowrap;
            text-decoration: none;
        }

        .custom-star-size svg {
            width: 13px;
            height: 13px;
            flex: 0 0 auto;
        }

        .star-size-loading {
            opacity: 0.65;
        }

        .custom-inactive-warning {
            box-sizing: border-box;
            background: var(--bgColor-danger-muted, #ffebe9);
            border: 1px solid var(--borderColor-danger-emphasis, #cf222e);
            color: var(--fgColor-danger, #d1242f);
            padding: 10px 12px;
            border-radius: 6px;
            margin: 10px 0;
            font-weight: 600;
            text-align: center;
            transition: opacity 0.5s ease;
        }

        .star-enhancer-avatar {
            display: inline-block;
            width: 20px;
            height: 20px;
            min-width: 20px;
            margin-right: 8px;
            border-radius: 50%;
            vertical-align: -5px;
            object-fit: cover;
            background: var(--bgColor-muted, #f6f8fa);
        }

        .star-enhancer-avatar-fallback {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 20px;
            height: 20px;
            min-width: 20px;
            margin-right: 8px;
            border-radius: 50%;
            vertical-align: -5px;
            background: var(--bgColor-muted, #f6f8fa);
            color: var(--fgColor-muted, #656d76);
            font-size: 10px;
            font-weight: 600;
        }

        .star-note-container {
            margin-top: 8px;
            font-size: 12px;
            display: flex;
            align-items: center;
            gap: 8px;
            min-height: 22px;
        }

        .star-note-text {
            color: var(--fgColor-accent, #0969da);
            cursor: pointer;
            border-bottom: 1px dashed currentColor;
            display: inline-block;
            padding: 2px 0;
            max-width: min(600px, 75vw);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .star-note-input {
            background: var(--bgColor-default, #fff);
            border: 1px solid var(--borderColor-default, #d0d7de);
            color: var(--fgColor-default, #1f2328);
            padding: 3px 7px;
            border-radius: 4px;
            width: min(300px, 50vw);
            box-sizing: border-box;
            display: none;
        }

        .star-note-input:focus {
            outline: 2px solid var(--focus-outlineColor, #0969da);
            outline-offset: -1px;
        }
    `);

    // ================= 状态与工具 =================
    const state = {
        observerTimer: null,
        starScanTimer: null,
        listScanTimer: null,
        repoKey: '',
        repoDataPromise: null,
        repoHeaderKey: '',
        starSizeQueueRunning: false
    };

    const repoDataInflight = new Map();

    function getRepoKeyFromPath() {
        const match = window.location.pathname.match(/^\/([^/]+)\/([^/]+)$/);
        if (!match) return '';
        const owner = decodeURIComponent(match[1]);
        const repo = decodeURIComponent(match[2]);
        if (repo === 'settings' || repo === 'issues' || repo === 'pulls' ||
            repo === 'actions' || repo === 'security' || repo === 'pulse' ||
            repo === 'graphs' || repo === 'network' || repo === 'commits') {
            // 这些路径可能并不代表仓库主页,交给下方更严格的判断。
        }
        return `${owner}/${repo}`;
    }

    function isRepoPage() {
        const path = window.location.pathname;
        return /^\/[^/]+\/[^/]+$/.test(path) && !/^\/[^/]+\/[^/]+\/(?!$)/.test(path);
    }

    function isStarsPage() {
        const path = window.location.pathname;
        const search = window.location.search;
        return path.includes('/stars') || /(?:^|[?&])tab=stars(?:&|$)/.test(search);
    }

    function createApiHeaders() {
        const headers = {
            'Accept': 'application/vnd.github+json'
        };
        if (GITHUB_TOKEN) {
            headers.Authorization = `Bearer ${GITHUB_TOKEN}`;
        }
        return headers;
    }

    // ================= GitHub API:带缓存 + 请求去重 =================
    async function fetchRepoData(owner, repo) {
        const cacheKey = `repo_data_${owner}/${repo}`;

        const cached = GM_getValue(cacheKey);
        if (cached && cached.timestamp && (Date.now() - cached.timestamp < CACHE_EXPIRE_TIME)) {
            return cached.data;
        }

        const inflightKey = `${owner}/${repo}`;
        if (repoDataInflight.has(inflightKey)) {
            return repoDataInflight.get(inflightKey);
        }

        const promise = (async () => {
            try {
                const res = await fetch(
                    `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
                    {
                        method: 'GET',
                        headers: createApiHeaders(),
                        cache: 'default'
                    }
                );

                if (!res.ok) {
                    const remaining = res.headers.get('X-RateLimit-Remaining');
                    const limit = res.headers.get('X-RateLimit-Limit');
                    console.warn(
                        `[GitHub Super Enhancer] API ${res.status} ` +
                        `(剩余额度 ${remaining != null ? remaining : '?'}/${limit != null ? limit : '?'}): ${owner}/${repo}`
                    );

                    // 403 / 429 基本是限流;给出一条明确的解决办法。
                    if (res.status === 403 || res.status === 429) {
                        console.warn(
                            '[GitHub Super Enhancer] 已触发 GitHub API 限流(未配置 Token 时约 60 次/小时)。' +
                            '请在脚本顶部 GITHUB_TOKEN 填入 Personal Access Token,可提升到 5000 次/小时。'
                        );
                    }

                    // 退回过期缓存:即使限流,标签也仍显示上次成功获取的大小。
                    if (cached && cached.data) {
                        return cached.data;
                    }
                    return null;
                }

                const data = await res.json();
                const compact = {
                    size: Number.isFinite(data.size) ? data.size : 0,
                    pushed_at: data.pushed_at || null
                };

                GM_setValue(cacheKey, {
                    timestamp: Date.now(),
                    data: compact
                });

                return compact;
            } catch (e) {
                console.error('[GitHub Super Enhancer] Fetch API Failed:', e);
                // 网络异常时同样退回过期缓存,保证标签不消失。
                if (cached && cached.data) {
                    return cached.data;
                }
                return null;
            } finally {
                repoDataInflight.delete(inflightKey);
            }
        })();

        repoDataInflight.set(inflightKey, promise);
        return promise;
    }

    function formatSize(kb) {
        if (!Number.isFinite(kb) || kb < 0) return '--';
        if (kb < 1024) return `${Math.round(kb)} KB`;

        const mb = kb / 1024;
        if (mb < 1024) return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`;

        const gb = mb / 1024;
        if (gb < 1024) return `${gb.toFixed(gb >= 100 ? 0 : 1)} GB`;

        const tb = gb / 1024;
        return `${tb.toFixed(tb >= 100 ? 0 : 1)} TB`;
    }

    // ================= 星标页:头像 + 备注 =================
    function getRepoCard(link) {
        return (
            link.closest('[data-testid="list-view-item"]') ||
            link.closest('article') ||
            link.closest('.Box-row') ||
            link.closest('[class*="Box-row"]') ||
            link.closest('li') ||
            link.parentElement?.parentElement ||
            link.parentElement
        );
    }

    function makeAvatar(owner) {
        const avatar = document.createElement('img');
        avatar.className = 'star-enhancer-avatar';
        avatar.alt = `${owner} avatar`;
        avatar.width = 20;
        avatar.height = 20;
        avatar.loading = 'lazy';
        avatar.decoding = 'async';
        avatar.referrerPolicy = 'no-referrer';
        avatar.src = `https://github.com/${encodeURIComponent(owner)}.png?size=40`;

        let failedOnce = false;
        avatar.addEventListener('error', () => {
            if (!failedOnce) {
                failedOnce = true;
                avatar.src = `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=40`;
                return;
            }

            const fallback = document.createElement('span');
            fallback.className = 'star-enhancer-avatar-fallback';
            fallback.textContent = owner.slice(0, 1).toUpperCase();
            fallback.title = owner;
            avatar.replaceWith(fallback);
        }, { once: false });

        return avatar;
    }

    function ensureStarAvatar(link, owner) {
        if (link.dataset.starAvatarReady === '1') return;

        const oldAvatar = link.querySelector(':scope > .star-enhancer-avatar, :scope > .star-enhancer-avatar-fallback');
        if (!oldAvatar) {
            link.insertBefore(makeAvatar(owner), link.firstChild);
        }

        link.dataset.starAvatarReady = '1';
    }

    function ensureStarNote(link, repoFullName) {
        const card = getRepoCard(link);
        if (!card || card.querySelector('.star-note-container')) return;

        const container = document.createElement('div');
        container.className = 'star-note-container';
        container.dataset.repo = repoFullName;

        const savedNote = GM_getValue(`note_${repoFullName}`, '');

        const noteDisplay = document.createElement('span');
        noteDisplay.className = 'star-note-text';
        noteDisplay.textContent = savedNote ? `📝 备注: ${savedNote}` : '备注';
        noteDisplay.title = savedNote || '点击添加备注';

        const input = document.createElement('input');
        input.className = 'star-note-input';
        input.type = 'text';
        input.value = savedNote;
        input.placeholder = '输入此仓库的用途...';
        input.autocomplete = 'off';

        const showInput = () => {
            noteDisplay.style.display = 'none';
            input.style.display = 'inline-block';
            input.focus();
            input.select();
        };

        const saveNote = () => {
            const value = input.value.trim();
            GM_setValue(`note_${repoFullName}`, value);

            if (value) {
                noteDisplay.textContent = `📝 备注: ${value}`;
                noteDisplay.title = value;
            } else {
                noteDisplay.textContent = '✏️ 添加备注';
                noteDisplay.title = '点击添加备注';
            }

            noteDisplay.style.display = 'inline-block';
            input.style.display = 'none';
        };

        noteDisplay.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            showInput();
        });

        input.addEventListener('blur', saveNote);
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                input.blur();
            } else if (e.key === 'Escape') {
                input.value = savedNote;
                input.blur();
            }
        });

        container.append(noteDisplay, input);

        // 尽量插到仓库标题所在的卡片下方,不再依赖旧版 .Box-row 结构。
        const heading = link.closest('h3');
        if (heading) {
            heading.insertAdjacentElement('afterend', container);
        } else {
            card.appendChild(container);
        }

        link.dataset.starNoteReady = '1';
    }

    // 创建“仓库大小”标签。主页和星标页共用同一套图标。
    function createSizeBadge(sizeKb, extraClass = '') {
        const badge = document.createElement('span');
        badge.className = `${extraClass || 'custom-header-size'}`.trim();
        badge.title = 'GitHub API 返回的仓库大小';

        badge.innerHTML = `
            <svg aria-hidden="true" viewBox="0 0 16 16" fill="currentColor">
                <path d="M8 1.75C4.4 1.75 1.75 3.02 1.75 4.75v6.5C1.75 12.98 4.4 14.25 8 14.25s6.25-1.27 6.25-3V4.75C14.25 3.02 11.6 1.75 8 1.75Zm4.75 9.5c0 .75-1.84 1.75-4.75 1.75s-4.75-1-4.75-1.75V9.55C4.36 10.2 6.04 10.5 8 10.5s3.64-.3 4.75-.95v1.7Zm0-3c0 .75-1.84 1.75-4.75 1.75s-4.75-1-4.75-1.75V6.55C4.36 7.2 6.04 7.5 8 7.5s3.64-.3 4.75-.95v1.7Zm0-3.25C12.75 5.75 10.91 6.75 8 6.75S3.25 5.75 3.25 5V4.75C3.25 4 5.09 3 8 3s4.75 1 4.75 1.75V5Z"></path>
            </svg>
            <span>${formatSize(sizeKb)}</span>
        `;

        return badge;
    }

    function ensureStarSizeBadge(link, repoFullName) {
        if (!link || !repoFullName) return;
        if (link.querySelector(':scope > .custom-star-size')) return;
        if (link.parentElement?.querySelector(':scope > .custom-star-size')) return;

        const badge = createSizeBadge(0, 'custom-star-size star-size-loading');
        badge.textContent = '加载中…';
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('aria-hidden', 'true');
        svg.setAttribute('viewBox', '0 0 16 16');
        svg.setAttribute('fill', 'currentColor');
        svg.innerHTML = '<path d="M8 1.75C4.4 1.75 1.75 3.02 1.75 4.75v6.5C1.75 12.98 4.4 14.25 8 14.25s6.25-1.27 6.25-3V4.75C14.25 3.02 11.6 1.75 8 1.75Zm4.75 9.5c0 .75-1.84 1.75-4.75 1.75s-4.75-1-4.75-1.75V9.55C4.36 10.2 6.04 10.5 8 10.5s3.64-.3 4.75-.95v1.7Zm0-3c0 .75-1.84 1.75-4.75 1.75s-4.75-1-4.75-1.75V6.55C4.36 7.2 6.04 7.5 8 7.5s3.64-.3 4.75-.95v1.7Zm0-3.25C12.75 5.75 10.91 6.75 8 6.75S3.25 5.75 3.25 5V4.75C3.25 4 5.09 3 8 3s4.75.75 4.75 1.75V5Z"></path>';
        badge.textContent = '';
        badge.append(svg, document.createTextNode('加载中…'));
        badge.dataset.starSizeRepo = repoFullName;
        badge.dataset.starSizeState = 'loading';

        // 最关键:直接插入仓库链接后面,确保一定是“仓库名后”。
        link.insertAdjacentElement('afterend', badge);
        return badge;
    }

    async function loadStarSizeBadge(link, repoFullName) {
        if (!link || !repoFullName) return;

        let badge = link.parentElement?.querySelector(':scope > .custom-star-size[data-star-size-repo="' + CSS.escape(repoFullName) + '"]');
        if (!badge) {
            badge = ensureStarSizeBadge(link, repoFullName);
        }
        if (!badge) return;

        if (badge.dataset.starSizeState === 'ready') return;
        if (badge.dataset.starSizeState === 'loading' && badge.dataset.starSizeStarted === '1') return;

        const [owner, repo] = repoFullName.split('/');
        if (!owner || !repo) return;

        badge.dataset.starSizeStarted = '1';

        const data = await fetchRepoData(owner, repo);

        // 页面可能已经发生 SPA 跳转,旧节点不再属于当前 DOM。
        if (!badge.isConnected) return;

        if (data && Number.isFinite(Number(data.size)) && Number(data.size) >= 0) {
            badge.className = 'custom-star-size';
            badge.title = `${repoFullName} · GitHub 仓库大小`;
            badge.replaceChildren();

            const fresh = createSizeBadge(Number(data.size), 'custom-star-size');
            badge.replaceWith(fresh);
            return;
        }

        // 请求失败时不长期占位;下次动态扫描可再次尝试。
        badge.remove();
    }

    async function processStarSizeQueue(links) {
        if (!Array.isArray(links) || !links.length) return;
        if (state.starSizeQueueRunning) return;

        state.starSizeQueueRunning = true;
        try {
            let index = 0;

            async function worker() {
                while (index < links.length) {
                    const link = links[index++];
                    if (!link?.isConnected) continue;

                    const href = link.getAttribute('href') || '';
                    const match = href.match(/^\/([^/]+)\/([^/?#]+)$/);
                    if (!match) continue;

                    const owner = decodeURIComponent(match[1]);
                    const repo = decodeURIComponent(match[2]);
                    await loadStarSizeBadge(link, `${owner}/${repo}`);
                }
            }

            const workers = Array.from(
                { length: Math.min(STAR_SIZE_CONCURRENCY, links.length) },
                () => worker()
            );

            await Promise.all(workers);
        } finally {
            state.starSizeQueueRunning = false;
        }
    }

    function processStarPage() {
        if (!isStarsPage()) return;

        // 不再只筛选“未处理头像/备注”的链接。
        // 因为星标页是动态列表,大小标签也必须独立判断。
        const allLinks = Array.from(document.querySelectorAll('h3 a[href^="/"]'));
        const repoLinks = [];

        allLinks.forEach((link) => {
            const href = link.getAttribute('href') || '';
            const match = href.match(/^\/([^/]+)\/([^/?#]+)$/);
            if (!match) return;

            const owner = decodeURIComponent(match[1]);
            const repo = decodeURIComponent(match[2]);
            const repoFullName = `${owner}/${repo}`;

            ensureStarAvatar(link, owner);
            ensureStarNote(link, repoFullName);

            // 只把还没有成功显示大小的仓库加入队列。
            const alreadyReady = link.parentElement?.querySelector(':scope > .custom-star-size:not(.star-size-loading)');
            if (!alreadyReady) {
                repoLinks.push(link);
            }
        });

        // 头像、备注立即处理;大小标签异步查询,不阻塞页面其它功能。
        if (repoLinks.length) {
            processStarSizeQueue(repoLinks);
        }
    }

    function scheduleStarScan() {
        if (!isStarsPage()) return;

        if (state.starScanTimer) {
            clearTimeout(state.starScanTimer);
        }

        state.starScanTimer = setTimeout(() => {
            state.starScanTimer = null;
            processStarPage();

            // GitHub 星标列表可能是异步渲染,轻量补扫,解决“首次打开头像/备注有时不出现”。
            STAR_SCAN_RETRY.slice(1).forEach((delay) => {
                window.setTimeout(() => {
                    if (isStarsPage()) processStarPage();
                }, delay);
            });
        }, 50);
    }

    // ================= 通用仓库列表页(探索 / 搜索 / 议题):大小标签 =================
    function isRepoListPage() {
        const path = window.location.pathname;
        // 探索页
        if (path === '/explore' || path.startsWith('/explore/')) return true;
        // 搜索页(代码 / 仓库 / 议题 / 提交 / 用户等所有 type)
        if (path === '/search' || path.startsWith('/search/')) return true;
        // 全局议题 / 拉取请求列表页
        if (path === '/issues' || path === '/pulls') return true;
        return false;
    }

    // GitHub 保留的“非仓库”第一段路径:这些永远不是 owner/repo。
    const RESERVED_OWNERS = new Set([
        'topics', 'collections', 'marketplace', 'sponsors', 'orgs', 'events', 'features',
        'apps', 'codespaces', 'pricing', 'about', 'contact', 'site', 'readme', 'login',
        'signup', 'account', 'settings', 'search', 'explore', 'issues', 'pulls',
        'notifications', 'new', 'watching', 'stars', 'followers', 'following',
        'repositories', 'packages', 'projects', 'discussions'
    ]);

    // 判断某个 /owner/repo 链接是否真的是“仓库名”链接。
    // 探索页只显示短名(无斜杠),搜索/议题页显示 owner/repo(有斜杠),
    // 因此结合多种信号判断,避免漏判或误判。
    function isRepoNameLink(a) {
        // 1) 文字显示为 “owner / repo”(含斜杠):搜索 / 议题 / 代码搜索页。
        const text = a.textContent.trim();
        if (text.includes('/')) return true;

        // 2) 链接位于标题元素里:探索页仓库卡片把仓库名放在 h2 里。
        if (a.closest('h1, h2, h3, h4, h5, h6')) return true;

        // 3) GitHub 埋点标记 click_target 为 REPOSITORY。
        const hydro = a.getAttribute('data-hydro-click') || '';
        if (/REPOSITORY/i.test(hydro)) return true;

        return false;
    }

    // 找出列表中所有“仓库名”链接(href 形如 /owner/repo)。
    function findRepoNameLinks() {
        const result = [];
        const anchors = document.querySelectorAll('a[href^="/"]');
        for (const a of anchors) {
            const href = a.getAttribute('href') || '';
            const match = href.match(/^\/([^/]+)\/([^/]+)$/);
            if (!match) continue;

            const owner = decodeURIComponent(match[1]);
            const repo = decodeURIComponent(match[2]);
            if (!owner || !repo) continue;

            // 排除 /topics/xxx、/collections/xxx 等保留路径。
            if (RESERVED_OWNERS.has(owner.toLowerCase())) continue;

            if (!isRepoNameLink(a)) continue;

            result.push(a);
        }
        return result;
    }

    function processRepoListPage() {
        if (!isRepoListPage()) return;

        const links = findRepoNameLinks();
        const toProcess = [];
        for (const link of links) {
            // 只把还没有成功显示大小的链接加入队列。
            const alreadyReady = link.parentElement?.querySelector(':scope > .custom-star-size:not(.star-size-loading)');
            if (!alreadyReady) {
                toProcess.push(link);
            }
        }

        if (toProcess.length) {
            processStarSizeQueue(toProcess);
        }
    }

    function scheduleListPageScan() {
        if (!isRepoListPage()) return;

        if (state.listScanTimer) {
            clearTimeout(state.listScanTimer);
        }

        state.listScanTimer = setTimeout(() => {
            state.listScanTimer = null;
            processRepoListPage();

            // 搜索 / 议题结果经常是异步渲染,补扫几次。
            [800, 2000, 4000].forEach((delay) => {
                window.setTimeout(() => {
                    if (isRepoListPage()) processRepoListPage();
                }, delay);
            });
        }, 120);
    }

    // ================= 仓库主页:不活跃警告 + 大小标签 =================
    function getRepositoryHeader() {
        // GitHub 2024 年改版后使用 data-testid="repo-header",
        // 旧的 #repository-container-header 已逐步下线,这里保留两者兼容。
        return (
            document.querySelector('[data-testid="repo-header"]') ||
            document.getElementById('repository-container-header') ||
            document.querySelector('[data-testid="repository-header"]') ||
            document.querySelector('header[class*="Header"]') ||
            document.querySelector('[data-testid="breadcrumbs"]') ||
            document.querySelector('main h1')?.closest('header') ||
            document.querySelector('main h1')?.parentElement
        );
    }

    // GitHub 页面结构经常调整,因此不再只依赖一个固定选择器。
    function getRepositoryTitle(header) {
        const repoKey = getRepoKeyFromPath();
        const [owner, repo] = repoKey ? repoKey.split('/') : ['', ''];

        // 1. 最优先:当前仓库链接所在的 h1。
        if (owner && repo) {
            const escapedOwner = CSS.escape(owner);
            const escapedRepo = CSS.escape(repo);

            const exactRepoLink =
                document.querySelector(`h1 a[href="/${escapedOwner}/${escapedRepo}"]`) ||
                document.querySelector(`h1 a[href^="/${escapedOwner}/${escapedRepo}"]`);

            if (exactRepoLink) {
                const h1 = exactRepoLink.closest('h1');
                if (h1) return h1;
            }
        }

        // 2. GitHub 常见结构(新布局 data-testid 与旧布局 id 都覆盖)。
        const selectors = [
            '#repository-container-header h1',
            '[data-testid="repository-header"] h1',
            '[data-testid="repo-header"] h1',
            '[data-testid="repo-title"]',
            'main h1'
        ];

        for (const selector of selectors) {
            const h1 = document.querySelector(selector);
            if (h1 && h1.textContent.trim()) return h1;
        }

        // 3. 最后兜底:从所有 h1 中寻找最像仓库名的标题。
        if (repo) {
            const h1s = document.querySelectorAll('h1');
            for (const h1 of h1s) {
                if (h1.textContent.includes(repo)) return h1;
            }
        }

        return null;
    }

    function findVisibilityLabel(header, title) {
        // 依次扩大搜索范围:标题 → header → 新布局头部容器 → 旧容器 → 面包屑。
        const scopes = [
            title,
            header,
            document.querySelector('[data-testid="repo-header"]'),
            document.getElementById('repository-container-header'),
            document.querySelector('[data-testid="breadcrumbs"]')
        ];

        // 1) 按 data-testid 精确定位。
        //    新布局的可见性胶囊带 data-testid="repo-visibility-label",
        //    不依赖界面语言(中文界面文字是“公共”,英文是“Public”)。
        for (const scope of scopes) {
            if (!scope) continue;

            const byTestid = scope.querySelector(
                '[data-testid="repo-visibility-label"], [data-testid="visibility-label"], [data-testid*="visibility"]'
            );
            if (byTestid) return byTestid;
        }

        // 1.5) 全文档按 testid 定位:新布局可能没有可用的 header 容器,
        //      data-testid 是权威标记,直接全文档查找最可靠。
        const docByTestid = document.querySelector(
            '[data-testid="repo-visibility-label"], [data-testid="visibility-label"]'
        );
        if (docByTestid) return docByTestid;

        // 2) 兜底:按标签文字匹配(兼容英文与中文界面:
        //    Public / Private / Internal / 公共 / 公开 / 私有 / 私人 / 内部)。
        const visibilityTest = /^(Public|Private|Internal|公共|公开|私有|私人|内部)( repository| template)?$/i;

        for (const scope of scopes) {
            if (!scope) continue;

            const candidates = scope.querySelectorAll(
                '.Label, [class*="Label--"], [class*="prc-Label"], span'
            );

            for (const el of candidates) {
                if (el.classList.contains('custom-header-size')) continue;

                const text = el.textContent.trim().replace(/\s+/g, ' ');
                if (visibilityTest.test(text)) {
                    return el;
                }
            }
        }

        // 3) 最后:全文档范围内只匹配“标签样式”的元素(避免误匹配正文)。
        const docCandidates = document.querySelectorAll('.Label, [class*="Label--"], [class*="prc-Label"]');
        for (const el of docCandidates) {
            if (el.classList.contains('custom-header-size')) continue;

            const text = el.textContent.trim().replace(/\s+/g, ' ');
            if (visibilityTest.test(text)) return el;
        }

        return null;
    }

    // 在仓库主页找到“仓库名”链接(头部面包屑里的 <a href="/owner/repo">),
    // 与星标页逻辑完全一致:锚点稳定,找到链接后直接在其后面插入大小标签。
    function findRepoTitleLink(owner, repo) {
        if (!owner || !repo) return null;

        const escapedOwner = CSS.escape(owner);
        const escapedRepo = CSS.escape(repo);

        const selectors = [
            `h1 a[href="/${escapedOwner}/${escapedRepo}"]`,
            // 新布局仓库名链接的权威标记(例如 data-testid="repo-name-link")。
            `[data-testid="repo-name-link"][href="/${escapedOwner}/${escapedRepo}"], [data-testid="repo-name-link"]`,
            `[data-testid="repo-title"] a[href="/${escapedOwner}/${escapedRepo}"]`,
            '[data-testid="repo-name-breadcrumb"]',
            `[data-testid="breadcrumbs"] a[href="/${escapedOwner}/${escapedRepo}"]`,
            `[data-testid="repo-header"] a[href="/${escapedOwner}/${escapedRepo}"]`,
            `#repository-container-header a[href="/${escapedOwner}/${escapedRepo}"]`
        ];

        for (const selector of selectors) {
            const el = document.querySelector(selector);
            if (el) return el;
        }

        // 兜底:页面中第一个指向该仓库的链接,通常就是头部面包屑里的仓库名。
        return document.querySelector(`a[href="/${escapedOwner}/${escapedRepo}"]`);
    }

    function applySizeBadge(repoData, repoKey) {
        const size = Number(repoData?.size);
        if (!Number.isFinite(size) || size < 0) return false;

        const [owner, repo] = repoKey ? repoKey.split('/') : ['', ''];
        if (!owner || !repo) return false;

        const titleLink = findRepoTitleLink(owner, repo);
        const header = getRepositoryHeader();
        const title = getRepositoryTitle(header);

        const badge = createSizeBadge(size, 'custom-header-size');
        badge.title = `${repoKey} · GitHub 仓库大小`;

        // 1) 首选:放到 Public / Private 可见性胶囊标签后面(与之前截图位置一致)。
        const visibilityLabel = findVisibilityLabel(header, title);

        // 调试日志:如果位置仍不对,把这条输出发我即可定位。
        console.info(
            '[GitHub Super Enhancer] 大小标签定位:',
            visibilityLabel ? '找到可见性标签 → 插到其后面' : '未找到可见性标签 → 退回仓库名链接后',
            { repoKey, foundLabel: !!visibilityLabel, foundTitleLink: !!titleLink, foundHeader: !!header }
        );

        if (visibilityLabel) {
            // 已经正确就位则跳过。
            if (visibilityLabel.nextElementSibling?.classList.contains('custom-header-size')) {
                return true;
            }
            // 旧版本可能把标签插到了仓库名链接后面(Public 标签前面),
            // 这里把头部区域内残留的标签全部移除,再重新插到正确位置。
            const region = header || document.querySelector('[data-testid="repo-header"]') || title || document.body;
            region.querySelectorAll('.custom-header-size').forEach((el) => el.remove());

            visibilityLabel.insertAdjacentElement('afterend', badge);
            return badge.isConnected;
        }

        // 2) 其次:新布局没有可见性标签时,直接放在仓库名链接后面。
        if (titleLink) {
            const linkParent = titleLink.parentElement;
            if (linkParent?.querySelector(':scope > .custom-header-size')) return true;

            titleLink.insertAdjacentElement('afterend', badge);
            if (badge.isConnected) return true;
        }

        // 3) 兜底:追加到 h1 最末尾。
        if (title) {
            if (title.querySelector(':scope > .custom-header-size')) return true;
            title.appendChild(badge);
            return badge.isConnected;
        }

        return false;
    }

    function applyInactiveWarning(repoData, repoKey) {
        const header = getRepositoryHeader();
        if (!header || !repoData?.pushed_at) return false;

        // 关键修复:
        // 不再用“DOM 中有没有 warning”判断。
        // 警告移除以后,MutationObserver 仍会收到 mutation,
        // 如果没有“已处理”标记,就会再次生成,形成无限循环。
        const handledKey = header.dataset.customInactiveHandled || '';
        if (handledKey === repoKey) return true;

        header.dataset.customInactiveHandled = repoKey;

        const oldWarning = header.parentElement?.querySelector('.custom-inactive-warning');
        if (oldWarning) oldWarning.remove();

        const lastPush = new Date(repoData.pushed_at);
        if (Number.isNaN(lastPush.getTime())) return true;

        const monthsDiff = (Date.now() - lastPush.getTime()) / (1000 * 60 * 60 * 24 * 30);

        if (monthsDiff <= INACTIVE_MONTHS) {
            return true;
        }

        const warning = document.createElement('div');
        warning.className = 'custom-inactive-warning';
        warning.dataset.repoKey = repoKey;

        warning.innerHTML =
            `⚠️ <b>不活跃警告:</b> 该仓库最后一次代码提交是在 ` +
            `<b>${lastPush.toLocaleDateString()}</b>,距今已有 ` +
            `<b>${Math.floor(monthsDiff)} 个月</b>未更新,可能已停止维护。`;

        const anchor = header.parentElement || header;
        if (header.nextSibling) {
            anchor.insertBefore(warning, header.nextSibling);
        } else {
            anchor.appendChild(warning);
        }

        window.setTimeout(() => {
            warning.style.opacity = '0';
            window.setTimeout(() => warning.remove(), 500);
        }, 5000);

        return true;
    }

    async function processRepoPage() {
        if (!isRepoPage()) {
            state.repoKey = '';
            state.repoDataPromise = null;
            state.repoHeaderKey = '';
            return;
        }

        const repoKey = getRepoKeyFromPath();
        if (!repoKey) return;

        const [owner, repo] = repoKey.split('/');
        if (!owner || !repo) return;

        // 同一仓库只发起一次 API 请求,不因 MutationObserver 反复触发。
        if (state.repoKey !== repoKey) {
            state.repoKey = repoKey;
            state.repoDataPromise = fetchRepoData(owner, repo);
            state.repoHeaderKey = '';
        }

        const repoData = await state.repoDataPromise;
        if (!repoData) return;

        // 大小标签只依赖“仓库名”链接,不依赖 header;
        // 即使 header 尚未渲染出来,也要先尝试插入。
        applySizeBadge(repoData, repoKey);

        // 不活跃警告需要 header;如果标题尚未由 GitHub 渲染出来,
        // 不锁死状态,让后面的 observer 再尝试。
        const header = getRepositoryHeader();
        if (header) {
            applyInactiveWarning(repoData, repoKey);
        }

        // 记录当前已经成功命中的 header,后续同一页面只做轻量检查。
        state.repoHeaderKey = repoKey;
    }

    function scheduleRepoEnhancement() {
        if (!isRepoPage()) return;

        if (state.observerTimer) {
            clearTimeout(state.observerTimer);
        }

        state.observerTimer = setTimeout(() => {
            state.observerTimer = null;
            processRepoPage();
        }, 80);
    }

    function runEnhancements() {
        if (isStarsPage()) {
            scheduleStarScan();
            return;
        }

        if (isRepoPage()) {
            scheduleRepoEnhancement();
            return;
        }

        if (isRepoListPage()) {
            scheduleListPageScan();
        }
    }

    // ================= 事件 / SPA 导航 =================
    function shouldRescanFromMutations(mutations) {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE) continue;

                if (isStarsPage()) {
                    if (
                        node.matches?.('h3, article, li, [data-testid="list-view-item"]') ||
                        node.querySelector?.('h3 a[href^="/"]')
                    ) {
                        return 'stars';
                    }
                }

                if (isRepoPage()) {
                    if (
                        node.id === 'repository-container-header' ||
                        node.matches?.('header, h1, [data-testid="repo-header"], [data-testid="repository-header"], [data-testid="repo-title"], [data-testid="breadcrumbs"]') ||
                        node.querySelector?.('#repository-container-header, h1, [data-testid="repo-header"], [data-testid="repository-header"], [data-testid="repo-title"], [data-testid="breadcrumbs"]')
                    ) {
                        return 'repo';
                    }
                }

                if (isRepoListPage()) {
                    if (
                        node.matches?.('a[href^="/"], article, li, [data-testid*="result"], [data-testid*="search"], [data-testid*="issue"], [data-testid*="repository"], [data-testid*="item"]') ||
                        node.querySelector?.('a[href^="/"]')
                    ) {
                        return 'list';
                    }
                }
            }
        }

        return '';
    }

    const observer = new MutationObserver((mutations) => {
        const type = shouldRescanFromMutations(mutations);

        if (type === 'stars') {
            scheduleStarScan();
        } else if (type === 'repo') {
            scheduleRepoEnhancement();
        } else if (type === 'list') {
            scheduleListPageScan();
        }
    });

    function startObserver() {
        if (!document.body) return;
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    document.addEventListener('turbo:render', () => {
        state.repoKey = '';
        state.repoDataPromise = null;
        state.repoHeaderKey = '';
        runEnhancements();
    });

    document.addEventListener('pjax:end', () => {
        state.repoKey = '';
        state.repoDataPromise = null;
        state.repoHeaderKey = '';
        runEnhancements();
    });

    window.addEventListener('pageshow', runEnhancements);

    // 首次加载
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            startObserver();
            runEnhancements();
        }, { once: true });
    } else {
        startObserver();
        runEnhancements();
    }
})();