GitHub DeepWiki Button

在 GitHub 仓库页面的 Star/Watch/Fork 按钮旁添加 DeepWiki 按钮及新窗口打开图标,方便一键跳转到对应仓库的 DeepWiki 页面。

// ==UserScript==
// @name         GitHub DeepWiki Button
// @namespace    http://tampermonkey.net/
// @version      0.11
// @description  在 GitHub 仓库页面的 Star/Watch/Fork 按钮旁添加 DeepWiki 按钮及新窗口打开图标,方便一键跳转到对应仓库的 DeepWiki 页面。
// @author       nuttycc
// @match        https://github.com/*/*
// @icon         https://deepwiki.com/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // 配置
  const CONFIG = {
    DEBUG: false, // 设置为 true 开启调试日志
    DEBOUNCE_DELAY: 300, // 防抖延迟时间(毫秒)
  };

  // 缓存常用数据
  const CACHE = {
    excludedPaths: [
      "settings",
      "marketplace",
      "explore",
      "topics",
      "trending",
      "collections",
      "events",
      "sponsors",
      "notifications",
    ],
    // 预定义 SVG 图标
    svgIcon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="margin-right:4px">
      <path d="M21 4H3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm-1 14H4V6h16v12z"></path>
      <path d="M7 9h10v1H7zm0 4h10v1H7z"></path>
    </svg>`,
    // 新窗口打开图标
    newWindowIcon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:middle">
      <path d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"></path>
    </svg>`,
  };

  // 日志函数,只在调试模式下输出
  function log(...args) {
    if (CONFIG.DEBUG) {
      console.debug("[DeepWiki]", ...args);
    }
  }

  // 防抖函数
  function debounce(func, delay) {
    let timer;
    return function (...args) {
      clearTimeout(timer);
      timer = setTimeout(() => func.apply(this, args), delay);
    };
  }

  // 获取仓库路径
  function getRepoPath() {
    const pathParts = window.location.pathname.split("/").filter(Boolean);
    if (pathParts.length < 2) return null;
    return `${pathParts[0]}/${pathParts[1]}`;
  }

  // 检查当前页面是否是仓库页面
  function isRepoPage() {
    // 快速检查 URL 格式
    const pathParts = window.location.pathname.split("/").filter(Boolean);

    // 必须至少有用户名和仓库名两部分
    if (pathParts.length < 2) {
      return false;
    }

    // 排除特殊页面
    if (CACHE.excludedPaths.includes(pathParts[0])) {
      return false;
    }

    // 检查是否存在 Star/Watch/Fork 按钮容器
    const buttonContainer = document.querySelector("ul.pagehead-actions");
    const isRepo = !!buttonContainer;

    log(
      `[isRepoPage] 检测仓库页面: 路径:${pathParts.join(
        "/"
      )}, 按钮容器是否存在:${isRepo}`
    );
    return isRepo;
  }

  // 创建 DeepWiki 按钮
  function createDeepWikiButton(repoPath) {
    // 使用 DocumentFragment 提高性能
    const fragment = document.createDocumentFragment();

    // 创建列表项
    const listItem = document.createElement("li");
    listItem.className = "deepwiki-button d-flex";
    listItem.style.display = "flex";
    listItem.style.alignItems = "center";

    // 创建主按钮
    const button = document.createElement("a");
    button.href = `https://deepwiki.com/${repoPath}`;
    button.className = "btn btn-sm";
    button.target = "_blank";
    button.rel = "noopener noreferrer";
    button.style.display = "flex";
    button.style.alignItems = "center";
    button.style.gap = "4px";
    button.style.borderTopRightRadius = "0";
    button.style.borderBottomRightRadius = "0";
    button.style.marginRight = "0";
    button.style.height = "28px"; // 固定高度

    // 使用预定义的 SVG 图标
    button.innerHTML = CACHE.svgIcon;

    // 添加文本
    const text = document.createElement("span");
    text.textContent = "DeepWiki";
    button.appendChild(text);

    // 创建新窗口打开按钮
    const newWindowButton = document.createElement("a");
    newWindowButton.href = `https://deepwiki.com/${repoPath}`;
    newWindowButton.className = "btn btn-sm";
    newWindowButton.target = "_blank";
    newWindowButton.rel = "noopener noreferrer";
    newWindowButton.title = "在新窗口中打开";
    newWindowButton.setAttribute("aria-label", "在新窗口中打开");
    newWindowButton.style.display = "flex";
    newWindowButton.style.alignItems = "center";
    newWindowButton.style.justifyContent = "center";
    newWindowButton.style.borderTopLeftRadius = "0";
    newWindowButton.style.borderBottomLeftRadius = "0";
    newWindowButton.style.borderLeft = "1px solid var(--color-border-default)";
    newWindowButton.style.padding = "5px 8px";
    newWindowButton.style.height = "28px"; // 确保与主按钮高度一致

    // 添加新窗口图标
    newWindowButton.innerHTML = CACHE.newWindowIcon;

    // 组装
    listItem.appendChild(button);
    listItem.appendChild(newWindowButton);
    fragment.appendChild(listItem);

    return fragment;
  }

  // 添加 DeepWiki 按钮
  function addDeepWikiButton() {
    // 如果按钮已存在,则不再添加
    if (document.querySelector(".deepwiki-button")) {
      log("按钮已存在,跳过添加。");
      return;
    }

    // 获取仓库路径
    const repoPath = getRepoPath();
    if (!repoPath) {
      log("路径不符合要求,跳过添加。");
      return;
    }

    log(`[addDeepWikiButton] 检测到仓库路径: ${repoPath}`);

    // 获取按钮容器
    const buttonContainer = document.querySelector("ul.pagehead-actions");
    if (!buttonContainer) {
      log("未找到 ul.pagehead-actions 容器,跳过添加。");
      return;
    }

    // 创建并添加按钮
    const buttonFragment = createDeepWikiButton(repoPath);
    buttonContainer.insertBefore(buttonFragment, buttonContainer.firstChild);

    log("🎉 按钮添加成功。");
  }

  // 处理页面变化的统一函数
  const handlePageChange = debounce(() => {
    if (isRepoPage()) {
      addDeepWikiButton();
    }
  }, CONFIG.DEBOUNCE_DELAY);

  // 初始化函数
  function init() {
    // 页面加载完成时检查
    window.addEventListener("load", () => {
      log("[event] load 事件触发");
      handlePageChange();
    });

    // 监听 PJAX 结束事件
    document.addEventListener("pjax:end", () => {
      log("[event] pjax:end 事件触发");
      handlePageChange();
    });

    // 监听 turbo:render 事件
    document.addEventListener("turbo:render", () => {
      log("[event] turbo:render 事件触发");
      handlePageChange();
    });

    // 使用 turbo:render 监听变化已经足够。故移除下面内容。
    // 使用更精确的 MutationObserver 监听 DOM 变化。
    // let lastUrl = location.href;
    // const urlObserver = new MutationObserver(() => {
    //   const url = location.href;
    //   if (url !== lastUrl) {
    //     lastUrl = url;
    //     log("[Observer CallBack] URL 变化:", url);
    //     handlePageChange();
    //   }
    // });

    // // 只观察 body 元素,减少不必要的回调
    // const observeTarget = document.querySelector("body");
    // if (observeTarget) {
    //   urlObserver.observe(observeTarget, {
    //     childList: true,
    //     subtree: true,
    //   });
    // }

    // 初始检查
    log("[init] 初始检查。");
    handlePageChange();
  }

  // 启动脚本
  init();
})();