github-jump-ai-read

在 GitHub 仓库页面 About 区域下方插入 DeepWiki、CodeWiki、Zread 三个外链按钮

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         github-jump-ai-read
// @name:zh-CN   一键跳转到 Github 项目 AI 阅读网站:DeepWiki、CodeWiki、Zread
// @namespace    https://github.com/Beeta/github-jump-ai-read
// @version      1.0.0
// @description  在 GitHub 仓库页面 About 区域下方插入 DeepWiki、CodeWiki、Zread 三个外链按钮
// @icon         https://avatars.githubusercontent.com/u/4686476?s=48&v=4
// @author       Beeta
// @match        https://github.com/*/*
// @run-at       document-idle
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const CONTAINER_ID = 'gh-external-links-injected';

    const BUTTONS_CONFIG = [
        { label: 'DeepWiki', getUrl: p => `https://deepwiki.com/${p}` },
        { label: 'CodeWiki', getUrl: p => `https://codewiki.google/github.com/${p}` },
        { label: 'Zread', getUrl: p => `https://zread.ai/${p}` },
    ];

    const BLACKLIST = [
        'settings', 'explore', 'marketplace', 'login', 'signup',
        'notifications', 'issues', 'pulls', 'sponsors', 'features',
        'pricing', 'about', 'contact', 'security', 'topics',
        'collections', 'trending', 'events', 'orgs',
    ];

    function extractRepoPath() {
        const parts = window.location.pathname.split('/').filter(Boolean);
        if (parts.length < 2) return null;

        const owner = parts[0];
        const repo = parts[1];

        // 过滤黑名单
        if (BLACKLIST.includes(owner.toLowerCase())) return null;
        if (BLACKLIST.includes(repo.toLowerCase())) return null;

        // owner 不能以 . 或 - 开头(GitHub 规则)
        if (/^[.\-]/.test(owner) || /^[.\-]/.test(repo)) return null;

        return `${owner}/${repo}`;
    }

    function findAboutSection() {
        // 策略 1:新版 GitHub data-testid
        const aboutSection = document.querySelector('[data-testid="about-section"]');
        if (aboutSection) return aboutSection;

        // 策略 2:BorderGrid-cell 中含 "About" 标题
        const cells = document.querySelectorAll('.BorderGrid-cell');
        for (const cell of cells) {
            const heading = cell.querySelector('h2, h3');
            if (heading && heading.textContent.trim() === 'About') {
                return cell;
            }
        }

        // 策略 3:全局搜索 h2/h3 文本为 "About" 的最近父容器
        const allHeadings = document.querySelectorAll('h2, h3');
        for (const h of allHeadings) {
            if (h.textContent.trim() === 'About') {
                return h.closest('div, section, aside') || h.parentElement;
            }
        }

        // 策略 4:兜底 sidebar
        return document.querySelector('.Layout-sidebar') ||
               document.querySelector('#repository-meta') ||
               null;
    }

    function injectStyles() {
        if (document.getElementById('gh-external-links-style')) return;
        const style = document.createElement('style');
        style.id = 'gh-external-links-style';
        style.textContent = `
            #gh-external-links-injected a {
                display: inline-block;
                padding: 4px 10px;
                border-radius: 6px;
                font-size: 12px;
                font-weight: 500;
                text-decoration: none;
                border: 1px solid var(--color-border-default, #d0d7de);
                color: var(--color-fg-default, #24292f);
                background-color: var(--color-canvas-subtle, #f6f8fa);
                cursor: pointer;
                line-height: 20px;
                transition: background-color 0.1s ease, border-color 0.1s ease;
            }
            #gh-external-links-injected a:hover {
                background-color: var(--color-canvas-inset, #eaeef2);
                border-color: var(--color-border-muted, #afb8c1);
            }
        `;
        document.head.appendChild(style);
    }

    function createButton(label, url) {
        const btn = document.createElement('a');
        btn.href = url;
        btn.target = '_blank';
        btn.rel = 'noopener noreferrer';
        btn.textContent = label;
        return btn;
    }

    function injectButtons() {
        // 幂等检查
        if (document.getElementById(CONTAINER_ID)) return;
        injectStyles();

        const repoPath = extractRepoPath();
        if (!repoPath) return;

        const aboutSection = findAboutSection();
        if (!aboutSection) return;

        const container = document.createElement('div');
        container.id = CONTAINER_ID;

        Object.assign(container.style, {
            display: 'flex',
            flexWrap: 'wrap',
            gap: '6px',
            marginTop: '12px',
            paddingTop: '12px',
            borderTop: '1px solid var(--color-border-muted, #d0d7de)',
        });

        for (const cfg of BUTTONS_CONFIG) {
            const url = cfg.getUrl(repoPath);
            const btn = createButton(cfg.label, url);
            container.appendChild(btn);
        }

        aboutSection.appendChild(container);
    }

    // -------- SPA 导航处理 --------

    let debounceTimer = null;

    function scheduleInject() {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
            // 移除旧容器(页面切换后重新注入)
            const old = document.getElementById(CONTAINER_ID);
            if (old) old.remove();
            injectButtons();
        }, 300);
    }

    function setupNavigationObserver() {
        // 保险 1:监听 <title> 变化 — 精准捕获 GitHub SPA 导航
        const titleEl = document.querySelector('title');
        if (titleEl) {
            new MutationObserver(scheduleInject).observe(titleEl, { childList: true });
        }

        // 保险 2:turbo / pjax 事件
        ['turbo:render', 'turbo:load', 'pjax:end'].forEach(evt => {
            document.addEventListener(evt, scheduleInject);
        });

        // 保险 3:监听 body — 兜底处理 React 局部渲染
        // 只在按钮容器不存在时才触发,避免 hover 引发 GitHub DOM 变化导致反复重注入
        new MutationObserver(mutations => {
            if (document.getElementById(CONTAINER_ID)) return;
            for (const m of mutations) {
                if (m.addedNodes.length > 0) {
                    scheduleInject();
                    break;
                }
            }
        }).observe(document.body, { childList: true, subtree: true });
    }

    // -------- 初始注入 --------

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(injectButtons, 500));
    } else {
        setTimeout(injectButtons, 500);
    }

    setupNavigationObserver();

})();