在 GitHub 仓库页面 About 区域下方插入 DeepWiki、CodeWiki、Zread 三个外链按钮
// ==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();
})();