GitHub Super Enhancer

GitHub 仓库增强:仓库主页、星标页(含“已星标 / 创建的”过滤清单)、探索/搜索/议题等列表页显示仓库大小、星标页作者头像与仓库说明备注、不活跃仓库警告;优化动态加载与 API 请求

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         GitHub Super Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.3.5
// @description  GitHub 仓库增强:仓库主页、星标页(含“已星标 / 创建的”过滤清单)、探索/搜索/议题等列表页显示仓库大小、星标页作者头像与仓库说明备注、不活跃仓库警告;优化动态加载与 API 请求
// @author       miscellaneouszx
// @match        https://github.com/*
// @icon         https://github.githubassets.com/favicons/favicon.png
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// @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 = 14 * 24 * 60 * 60 * 1000; // 仓库数据缓存 14 天(大小变化慢,加长可明显减少 API 调用)
    const INACTIVE_MONTHS = 6;                            // 超过多少个月没有 push 才提示
    const STAR_SCAN_RETRY = [0, 500, 1500, 3500];       // 星标页异步加载时,补扫几次
    const STAR_SIZE_CONCURRENCY = 4;                       // 同时查询 GitHub API 的数量
    const LAZY_LOAD_MARGIN = 1000;                        // 懒加载提前量(px):滚动到距视口 1000px 内才发起查询
    const FAIL_COOLDOWN_MS = 5 * 60 * 1000;              // 单个仓库查询失败后的冷却时间(避免反复重试)
    const RATE_LIMIT_LOG_DELAY = 4000;                   // 打印 API 额度前的延迟(ms):等本页其它查询先消耗额度,打印实时剩余额度
    // ==========================================

    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: 30px;
            height: 30px;
            min-width: 30px;
            margin-right: 8px;
            border-radius: 50%;
            vertical-align: -7px;
            object-fit: cover;
            background: var(--bgColor-muted, #f6f8fa);
        }

        .star-enhancer-avatar-fallback {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 30px;
            height: 30px;
            min-width: 30px;
            margin-right: 8px;
            border-radius: 50%;
            vertical-align: -7px;
            background: var(--bgColor-muted, #f6f8fa);
            color: var(--fgColor-muted, #656d76);
            font-size: 15px;
            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();

    // 限流 / 失败冷却 / 懒加载队列
    let rateLimitedUntil = 0;            // 全局限流冷却截止时间(毫秒时间戳),期间不再发请求
    let rateLimitRescanScheduled = false;
    let lastRateLimit = null;            // 最近一次 API 响应头记录的额度(/rate_limit 失败时兜底显示)
    let quotaPrintTimer = null;          // 浏览消耗额度后的刷新打印防抖定时器
    let lastPrintedQuotaKey = '';        // 已打印额度的去重键(limit:remaining:reset),数值无变化不重复打印
    const repoFailCooldown = new Map();  // repo → 查询失败后的冷却截止时间(毫秒时间戳)
    const starSizePending = [];          // 待查询的仓库链接队列

    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;
    }

    // 从任意 api.github.com 响应头记录限流信息(X-RateLimit-* 每个 API 响应都会带)。
    // 用于 /rate_limit 请求失败时兜底显示剩余额度。
    function recordRateLimitFromHeaders(headers) {
        if (!headers) return;
        const limit = Number(headers.get('X-RateLimit-Limit'));
        const remaining = Number(headers.get('X-RateLimit-Remaining'));
        const reset = Number(headers.get('X-RateLimit-Reset'));
        if (Number.isFinite(limit) && limit >= 0) {
            lastRateLimit = {
                limit,
                remaining: Number.isFinite(remaining) && remaining >= 0 ? remaining : limit,
                reset: Number.isFinite(reset) && reset > 0 ? reset : 0
            };
            // 每次真实 API 响应都触发防抖刷新:浏览未缓存仓库消耗额度后,控制台立即更新。
            scheduleQuotaRefresh();
        }
    }

    // GM_xmlhttpRequest 的 responseHeaders 是原始字符串("Name: value\r\n..."),
    // 转成 recordRateLimitFromHeaders 需要的 { get(name) } 形式。
    function recordRateLimitFromRawHeaders(headersText) {
        if (!headersText) return;
        recordRateLimitFromHeaders({
            get: (name) => {
                const m = headersText.match(new RegExp('(?:^|\\r?\\n)' + name + ': ([^\\r\\n]*)', 'i'));
                return m ? m[1].trim() : null;
            }
        });
    }

    // 请求 /rate_limit 获取实时额度(该接口本身不消耗额度)。
    // 优先用 GM_xmlhttpRequest(油猴特权请求):
    //   - 完全绕过浏览器 CORS 预检,带 Token(Authorization 头)时也稳定;
    //   - 不经过页面 HTTP 缓存层,配 cache: false 后每次都是实时值,
    //     不会被 GitHub 响应头 Cache-Control: private, max-age=60 的缓存拖住。
    // 普通 fetch 作为无 GM_xmlhttpRequest 的脚本管理器下的兜底。
    function fetchRateLimit() {
        const url = `https://api.github.com/rate_limit?_=${Date.now()}`;
        const headers = createApiHeaders();

        const xmlhttp = typeof GM_xmlhttpRequest === 'function'
            ? GM_xmlhttpRequest
            : (typeof GM !== 'undefined' && GM.xmlhttpRequest ? GM.xmlhttpRequest.bind(GM) : null);

        if (xmlhttp) {
            return new Promise((resolve) => {
                xmlhttp({
                    method: 'GET',
                    url,
                    headers,
                    cache: false,
                    timeout: 10000,
                    onload: (res) => {
                        recordRateLimitFromRawHeaders(res.responseHeaders);
                        if (res.status >= 200 && res.status < 300) {
                            try {
                                const data = JSON.parse(res.responseText);
                                const core = data && data.resources && data.resources.core;
                                if (core && core.limit != null) {
                                    resolve({
                                        used: core.used,
                                        limit: core.limit,
                                        remaining: core.remaining,
                                        reset: core.reset
                                    });
                                    return;
                                }
                            } catch (e) { /* 解析失败走下方 resolve(null) */ }
                        }
                        resolve(null);
                    },
                    onerror: () => resolve(null),
                    ontimeout: () => resolve(null)
                });
            });
        }

        // 无 GM_xmlhttpRequest 的环境:退回普通 fetch,只带 CORS 安全请求头,避免预检。
        return fetch(url, { method: 'GET', headers, cache: 'no-store' })
            .then((res) => {
                recordRateLimitFromHeaders(res.headers);
                return res.json();
            })
            .then((data) => {
                const core = data && data.resources && data.resources.core;
                if (core && core.limit != null) {
                    return { used: core.used, limit: core.limit, remaining: core.remaining, reset: core.reset };
                }
                return null;
            })
            .catch(() => null);
    }

    // ================= 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'
                    }
                );

                // 记录本次响应头里的限流信息,供 /rate_limit 失败时兜底。
                recordRateLimitFromHeaders(res.headers);

                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:优先按 Retry-After / X-RateLimit-Reset 计算冷却时间,
                    // 期间全局暂停查询,冷却结束自动重扫恢复。
                    if (res.status === 403 || res.status === 429) {
                        const retryAfter = Number(res.headers.get('Retry-After'));
                        const reset = Number(res.headers.get('X-RateLimit-Reset'));
                        let waitMs = 60 * 1000;
                        if (retryAfter > 0) waitMs = retryAfter * 1000;
                        else if (reset > 0) waitMs = reset * 1000 - Date.now();
                        waitMs = Math.max(waitMs, 5 * 1000);

                        rateLimitedUntil = Date.now() + waitMs;
                        console.warn(
                            '[GitHub Super Enhancer] 已触发 GitHub API 限流(可能是次级限流,额度未用完也会触发),' +
                            `将在约 ${Math.ceil(waitMs / 1000)} 秒后自动恢复,期间暂停查询。`
                        );
                        scheduleRateLimitRescan(waitMs);
                    }

                    // 单仓库失败冷却:失败后 5 分钟内不再重复查询它。
                    repoFailCooldown.set(inflightKey, Date.now() + FAIL_COOLDOWN_MS);

                    // 退回过期缓存:即使限流,标签也仍显示上次成功获取的大小。
                    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
                };

                // 查询成功:清除该仓库的失败冷却。
                repoFailCooldown.delete(inflightKey);

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

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

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

    // 限流冷却结束后,自动重新扫描当前页面,让“加载中”的标签继续完成。
    function scheduleRateLimitRescan(waitMs) {
        if (rateLimitRescanScheduled) return;
        rateLimitRescanScheduled = true;

        window.setTimeout(() => {
            rateLimitRescanScheduled = false;
            if (isStarsPage()) processStarPage();
            if (isRepoListPage()) processRepoListPage();
            if (isRepoPage()) scheduleRepoEnhancement();
        }, waitMs + 1000);
    }

    // 统一打印额度信息;同时记录去重键,避免防抖刷新重复打印同一数值。
    function printQuotaInfo(used, limit, remaining, reset, sourceNote) {
        lastPrintedQuotaKey = `${limit}:${remaining}:${reset}`;
        const resetAt = reset ? new Date(reset * 1000).toLocaleTimeString() : '?';
        console.info(
            `[GitHub Super Enhancer] API 额度:核心 ${used}/${limit}` +
            `(剩余 ${remaining},约 ${resetAt} 重置)` +
            (GITHUB_TOKEN ? '' : ';未配置 Token,额度较低(60 次/小时),建议配置') +
            (sourceNote ? `(${sourceNote})` : '')
        );
    }

    // 浏览过程中每消耗一次额度(任意 API 响应返回)都会触发:
    // 防抖 1.2 秒后打印一次最新剩余额度;数值无变化则不重复打印(避免刷屏)。
    function scheduleQuotaRefresh() {
        if (quotaPrintTimer) clearTimeout(quotaPrintTimer);
        quotaPrintTimer = window.setTimeout(() => {
            quotaPrintTimer = null;
            if (lastRateLimit && lastRateLimit.limit != null) {
                const key = `${lastRateLimit.limit}:${lastRateLimit.remaining}:${lastRateLimit.reset}`;
                if (key === lastPrintedQuotaKey) return;
                printQuotaInfo(
                    lastRateLimit.limit - lastRateLimit.remaining,
                    lastRateLimit.limit,
                    lastRateLimit.remaining,
                    lastRateLimit.reset
                );
            }
        }, 1200);
    }

    // 打印当前 API 额度(rate_limit 接口本身不消耗额度)。
    // 说明:
    //   1) 原实现每次刷新都显示满额度(5000):GitHub 的 /rate_limit 响应头带
    //      Cache-Control: private, max-age=60,浏览器/代理层会按 URL 缓存响应,
    //      60 秒内重复请求可能命中缓存拿到旧值(表现为“剩余额度一直停在同一个数”)。
    //      这里给 URL 追加时间戳参数,并改用 GM_xmlhttpRequest(cache: false),
    //      彻底绕开所有缓存层,每次都是实时值。
    //   2) 原实现页面加载后立即请求,此时本页其它仓库查询还没消耗额度,打印的剩余额度
    //      看起来"没有即时更新"。改为延迟 RATE_LIMIT_LOG_DELAY 毫秒,等本页主要查询完成后,
    //      打印的是包含本次页面浏览消耗的真实剩余额度。
    //   3) 曾给请求额外加 Cache-Control / Pragma 请求头试图绕缓存——这两个头不在
    //      CORS 安全请求头白名单里(实测 api.github.com 的 Access-Control-Allow-Headers
    //      只放行 Authorization、Content-Type 等,不含 cache-control/pragma),跨域请求
    //      预检失败导致 TypeError: Failed to fetch。因此额度请求改用 GM_xmlhttpRequest
    //      特权请求,完全不走 CORS 预检;普通 fetch 兜底时也只带安全请求头。
    //   4) 即使 /rate_limit 仍偶发失败,也优先用本页其它 API 响应头里的 X-RateLimit-*
    //      打印剩余额度,保证额度信息即时可见,而不是只报错。
    //   5) 除页面加载 / SPA 跳转外,浏览“未缓存”的仓库会真实消耗额度:每次 API 响应
    //      都会触发 scheduleQuotaRefresh 防抖刷新(1.2 秒),数值有变化即打印更新后的
    //      剩余额度;浏览“已缓存”仓库不发起请求,既不消耗额度也不打印。
    function logRateLimitStatus() {
        window.setTimeout(async () => {
            const quota = await fetchRateLimit();
            if (quota) {
                // 与最近一次“随浏览刷新”打印的数值相同则跳过,避免同一数值重复刷屏。
                if (`${quota.limit}:${quota.remaining}:${quota.reset}` !== lastPrintedQuotaKey) {
                    printQuotaInfo(quota.used, quota.limit, quota.remaining, quota.reset);
                }
                return;
            }

            // /rate_limit 请求失败(网络波动等)时,退回最近一次 API 响应头里的额度,
            // 保证剩余额度仍然即时可见,不再只报错。
            if (lastRateLimit && lastRateLimit.limit != null) {
                const key = `${lastRateLimit.limit}:${lastRateLimit.remaining}:${lastRateLimit.reset}`;
                if (key === lastPrintedQuotaKey) return;
                console.warn('[GitHub Super Enhancer] /rate_limit 请求失败,改用最近一次 API 响应头显示额度');
                printQuotaInfo(
                    lastRateLimit.limit - lastRateLimit.remaining,
                    lastRateLimit.limit,
                    lastRateLimit.remaining,
                    lastRateLimit.reset,
                    '最近一次 API 响应头'
                );
            } else {
                console.warn('[GitHub Super Enhancer] 获取 API 额度失败(不影响其它功能)');
            }
        }, RATE_LIMIT_LOG_DELAY);
    }

    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 = 30;
        avatar.height = 30;
        avatar.loading = 'lazy';
        avatar.decoding = 'async';
        avatar.referrerPolicy = 'no-referrer';
        avatar.src = `https://github.com/${encodeURIComponent(owner)}.png?size=60`;

        let failedOnce = false;
        avatar.addEventListener('error', () => {
            if (!failedOnce) {
                failedOnce = true;
                avatar.src = `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=60`;
                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;

        // 全局限流冷却中:先不查询,等冷却结束后由自动重扫继续。
        if (Date.now() < rateLimitedUntil) return;

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

        // 该仓库刚查询失败过,冷却中:跳过(标签保留“加载中”,冷却后重扫继续)。
        if (Date.now() < (repoFailCooldown.get(repoFullName) || 0)) 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();
    }

    // 把仓库链接加入查询队列,并尝试开始处理(队列可随时追加,不丢链接)。
    function enqueueStarSizeLinks(links) {
        if (!Array.isArray(links)) return;
        for (const link of links) {
            if (link?.isConnected) starSizePending.push(link);
        }
        drainStarSizeQueue();
    }

    async function drainStarSizeQueue() {
        if (state.starSizeQueueRunning) return;
        if (!starSizePending.length) return;

        state.starSizeQueueRunning = true;
        try {
            const links = starSizePending.splice(0);
            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;
            // 处理期间可能又有新的入队,继续排空。
            if (starSizePending.length) drainStarSizeQueue();
        }
    }

    // 判断元素是否接近当前视口(含提前量,提前量内就开始加载)。
    function isNearViewport(el) {
        const rect = el.getBoundingClientRect();
        const margin = LAZY_LOAD_MARGIN;
        return rect.bottom >= -margin && rect.top <= window.innerHeight + margin;
    }

    // 懒加载:滚动到可视区附近才真正发起查询,节省 API 额度。
    const sizeLazyObserver = new IntersectionObserver((entries) => {
        const toEnqueue = [];
        for (const entry of entries) {
            if (!entry.isIntersecting) continue;
            const link = entry.target;
            sizeLazyObserver.unobserve(link);
            toEnqueue.push(link);
        }
        if (toEnqueue.length) enqueueStarSizeLinks(toEnqueue);
    }, { rootMargin: `${LAZY_LOAD_MARGIN}px 0px ${LAZY_LOAD_MARGIN}px 0px` });

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

        // 不再只筛选“未处理头像/备注”的链接。
        // 因为星标页是动态列表,大小标签也必须独立判断。
        //
        // 星标页包含“已星标(Starred)”与“创建的(Created)”两种清单:
        // 经典布局里两者的仓库名都在 h3 中;新版布局(list-view)可能把仓库名放在
        // div[data-testid="list-view-item-title-container"] 的 h4 里。
        // 因此统一收集“仓库名链接”(href 形如 /owner/repo,下面再用正则过滤),并去重。
        const candidates = document.querySelectorAll(
            'h3 a[href^="/"], ' +
            '#user-starred-repos a[href^="/"], ' +
            '[data-testid="list-view-item"] a[href^="/"], ' +
            '[data-testid="list-view-item-title-container"] a[href^="/"]'
        );
        const seen = new Set(candidates);
        const allLinks = Array.from(candidates);
        // 兜底:再补一轮通用“仓库名链接”探测(标题层级 / 文字含斜杠 / 埋点标记),
        // 防止 GitHub 改版后“创建的”清单换成其它容器结构而漏掉。
        for (const link of findRepoNameLinks()) {
            if (seen.has(link)) continue;
            seen.add(link);
            allLinks.push(link);
        }
        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) return;

            // 懒加载:只查询接近视口的仓库,其余等滚动到附近再查询(省 API 额度)。
            if (isNearViewport(link)) {
                repoLinks.push(link);
            } else {
                sizeLazyObserver.observe(link);
            }
        });

        // 头像、备注立即处理;大小标签异步查询,不阻塞页面其它功能。
        if (repoLinks.length) {
            enqueueStarSizeLinks(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) continue;

            // 懒加载:只查询接近视口的仓库,其余等滚动到附近再查询。
            if (isNearViewport(link)) {
                toProcess.push(link);
            } else {
                sizeLazyObserver.observe(link);
            }
        }

        if (toProcess.length) {
            enqueueStarSizeLinks(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, h4, article, li, [data-testid="list-view-item"], [data-testid="list-view-item-title-container"]') ||
                        node.querySelector?.('h3 a[href^="/"], h4 a[href^="/"], [data-testid="list-view-item-title-container"] a[href^="/"]') ||
                        (node.matches?.('a[href^="/"]') && /^\/([^/]+)\/([^/?#]+)$/.test(node.getAttribute('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();
        // SPA 跳转后重新打印实时额度(rate_limit 不消耗额度,成本可忽略)。
        logRateLimitStatus();
    });

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

    window.addEventListener('pageshow', runEnhancements);

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