GitHub DeepWiki Button

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

  1. // ==UserScript==
  2. // @name GitHub DeepWiki Button
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.12
  5. // @description 在 GitHub 仓库页面的 Star/Watch/Fork 按钮旁添加 DeepWiki 按钮及新窗口打开图标,方便一键跳转到对应仓库的 DeepWiki 页面。
  6. // @author nuttycc
  7. // @match https://github.com/*/*
  8. // @icon https://deepwiki.com/favicon.ico
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. "use strict";
  15.  
  16. // 配置
  17. const CONFIG = {
  18. DEBUG: false, // 设置为 true 开启调试日志
  19. DEBOUNCE_DELAY: 300, // 防抖延迟时间(毫秒)
  20. };
  21.  
  22. // 缓存常用数据
  23. const CACHE = {
  24. excludedPaths: [
  25. "settings",
  26. "marketplace",
  27. "explore",
  28. "topics",
  29. "trending",
  30. "collections",
  31. "events",
  32. "sponsors",
  33. "notifications",
  34. ],
  35. // 预定义 SVG 图标
  36. svgIcon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="margin-right:4px">
  37. <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>
  38. <path d="M7 9h10v1H7zm0 4h10v1H7z"></path>
  39. </svg>`,
  40. // 新窗口打开图标
  41. newWindowIcon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:middle">
  42. <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>
  43. </svg>`,
  44. };
  45.  
  46. // 日志函数,只在调试模式下输出
  47. function log(...args) {
  48. if (CONFIG.DEBUG) {
  49. console.debug("[DeepWiki]", ...args);
  50. }
  51. }
  52.  
  53. // 防抖函数
  54. function debounce(func, delay) {
  55. let timer;
  56. return function (...args) {
  57. clearTimeout(timer);
  58. timer = setTimeout(() => func.apply(this, args), delay);
  59. };
  60. }
  61.  
  62. // 获取仓库路径
  63. function getRepoPath() {
  64. const pathParts = window.location.pathname.split("/").filter(Boolean);
  65. if (pathParts.length < 2) return null;
  66. return `${pathParts[0]}/${pathParts[1]}`;
  67. }
  68.  
  69. // 检查当前页面是否是仓库页面
  70. function isRepoPage() {
  71. // 快速检查 URL 格式
  72. const pathParts = window.location.pathname.split("/").filter(Boolean);
  73.  
  74. // 必须至少有用户名和仓库名两部分
  75. if (pathParts.length < 2) {
  76. return false;
  77. }
  78.  
  79. // 排除特殊页面
  80. if (CACHE.excludedPaths.includes(pathParts[0])) {
  81. return false;
  82. }
  83.  
  84. // 检查是否存在 Star/Watch/Fork 按钮容器
  85. const buttonContainer = document.querySelector("ul.pagehead-actions");
  86. const isRepo = !!buttonContainer;
  87.  
  88. log(
  89. `[isRepoPage] 检测仓库页面: 路径:${pathParts.join(
  90. "/"
  91. )}, 按钮容器是否存在:${isRepo}`
  92. );
  93. return isRepo;
  94. }
  95.  
  96. // 创建 DeepWiki 按钮
  97. function createDeepWikiButton(repoPath) {
  98. // 使用 DocumentFragment 提高性能
  99. const fragment = document.createDocumentFragment();
  100.  
  101. // 创建列表项
  102. const listItem = document.createElement("li");
  103. listItem.className = "deepwiki-button d-flex";
  104. listItem.style.display = "flex";
  105. listItem.style.alignItems = "center";
  106.  
  107. // 创建主按钮
  108. const button = document.createElement("a");
  109. button.href = `https://deepwiki.com/${repoPath}`;
  110. button.className = "btn btn-sm";
  111. button.target = "_blank";
  112. button.rel = "noopener noreferrer";
  113. button.style.display = "flex";
  114. button.style.alignItems = "center";
  115. button.style.gap = "4px";
  116. button.style.borderTopRightRadius = "0";
  117. button.style.borderBottomRightRadius = "0";
  118. button.style.marginRight = "0";
  119. button.style.height = "28px"; // 固定高度
  120.  
  121. // 使用预定义的 SVG 图标
  122. button.innerHTML = CACHE.svgIcon;
  123.  
  124. // 添加文本
  125. const text = document.createElement("span");
  126. text.textContent = "DeepWiki";
  127. button.appendChild(text);
  128.  
  129. // 创建新窗口打开按钮
  130. const newWindowButton = document.createElement("a");
  131. newWindowButton.href = `https://deepwiki.com/${repoPath}`;
  132. newWindowButton.className = "btn btn-sm";
  133. newWindowButton.target = "_blank";
  134. newWindowButton.rel = "noopener noreferrer";
  135. newWindowButton.title = "在新窗口中打开";
  136. newWindowButton.setAttribute("aria-label", "在新窗口中打开");
  137. newWindowButton.style.display = "flex";
  138. newWindowButton.style.alignItems = "center";
  139. newWindowButton.style.justifyContent = "center";
  140. newWindowButton.style.borderTopLeftRadius = "0";
  141. newWindowButton.style.borderBottomLeftRadius = "0";
  142. newWindowButton.style.borderLeft = "1px solid var(--color-border-default)";
  143. newWindowButton.style.padding = "5px 8px";
  144. newWindowButton.style.height = "28px"; // 确保与主按钮高度一致
  145.  
  146. // 添加新窗口图标
  147. newWindowButton.innerHTML = CACHE.newWindowIcon;
  148.  
  149. // 组装
  150. listItem.appendChild(button);
  151. listItem.appendChild(newWindowButton);
  152. fragment.appendChild(listItem);
  153.  
  154. return fragment;
  155. }
  156.  
  157. // 添加 DeepWiki 按钮
  158. function addDeepWikiButton() {
  159. // 如果按钮已存在,则不再添加
  160. if (document.querySelector(".deepwiki-button")) {
  161. log("按钮已存在,跳过添加。");
  162. return;
  163. }
  164.  
  165. // 获取仓库路径
  166. const repoPath = getRepoPath();
  167. if (!repoPath) {
  168. log("路径不符合要求,跳过添加。");
  169. return;
  170. }
  171.  
  172. log(`[addDeepWikiButton] 检测到仓库路径: ${repoPath}`);
  173.  
  174. // 获取按钮容器
  175. const buttonContainer = document.querySelector("ul.pagehead-actions");
  176. if (!buttonContainer) {
  177. log("未找到 ul.pagehead-actions 容器,跳过添加。");
  178. return;
  179. }
  180.  
  181. // 创建并添加按钮
  182. const buttonFragment = createDeepWikiButton(repoPath);
  183. buttonContainer.insertBefore(buttonFragment, buttonContainer.firstChild);
  184.  
  185. log("🎉 按钮添加成功。");
  186. }
  187.  
  188. // 处理页面变化的统一函数
  189. const handlePageChange = debounce(() => {
  190. if (isRepoPage()) {
  191. addDeepWikiButton();
  192. }
  193. }, CONFIG.DEBOUNCE_DELAY);
  194.  
  195. // 初始化函数
  196. function init() {
  197. // 页面加载完成时检查
  198. window.addEventListener("load", () => {
  199. log("[event] load 事件触发");
  200. handlePageChange();
  201. });
  202.  
  203. // 监听 PJAX 结束事件
  204. document.addEventListener("pjax:end", () => {
  205. log("[event] pjax:end 事件触发");
  206. handlePageChange();
  207. });
  208.  
  209. // 监听 turbo:render 事件
  210. document.addEventListener("turbo:render", () => {
  211. log("[event] turbo:render 事件触发");
  212. handlePageChange();
  213. });
  214.  
  215. // 使用 turbo:render 监听变化已经足够。故移除下面内容。
  216. // 使用更精确的 MutationObserver 监听 DOM 变化。
  217. // let lastUrl = location.href;
  218. // const urlObserver = new MutationObserver(() => {
  219. // const url = location.href;
  220. // if (url !== lastUrl) {
  221. // lastUrl = url;
  222. // log("[Observer CallBack] URL 变化:", url);
  223. // handlePageChange();
  224. // }
  225. // });
  226.  
  227. // // 只观察 body 元素,减少不必要的回调
  228. // const observeTarget = document.querySelector("body");
  229. // if (observeTarget) {
  230. // urlObserver.observe(observeTarget, {
  231. // childList: true,
  232. // subtree: true,
  233. // });
  234. // }
  235.  
  236. // 初始检查
  237. log("[init] 初始检查。");
  238. handlePageChange();
  239. }
  240.  
  241. // 启动脚本
  242. init();
  243. })();