DeepWiki Button Enhanced

Adds a DeepWiki button to GitHub repository pages, linking to deepwiki.com/{user}/{repo}. Combines features and optimizes previous scripts.

当前为 2025-04-29 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         DeepWiki Button Enhanced
// @namespace    https://github.com/zhsama/deepwiki.git
// @version      1.0
// @description  Adds a DeepWiki button to GitHub repository pages, linking to deepwiki.com/{user}/{repo}. Combines features and optimizes previous scripts.
// @author       zhsama
// @match        https://github.com/*/*
// @grant        none
// @license      MIT
// @icon         https://deepwiki.com/icon.png?66aaf51e0e68c818
// @supportURL   https://github.com/zhsama/deepwiki
// @homepageURL  https://github.com/zhsama/deepwiki
// ==/UserScript==

(function () {
  'use strict';

  const BUTTON_ID = 'deepwiki-button-enhanced';
  let lastUrl = location.href; // Track URL for SPA navigation changes

  // --- Logging ---
  const log = (...args) => console.log('[DeepWiki Button]', ...args);
  const errorLog = (...args) => console.error('[DeepWiki Button]', ...args);

  // --- Page Detection ---
  /**
   * Checks if the current page is a GitHub repository page (not settings, issues list, etc.)
   * @returns {boolean} True if it's a repository page, false otherwise.
   */
  function isRepoPage() {
    try {
      // Basic check: path must have at least user/repo
      const pathParts = window.location.pathname.split('/').filter(Boolean);
      if (pathParts.length < 2) return false;

      // Exclude common non-repo pages that match the basic path structure
      const nonRepoPaths = [
        '/settings', '/issues', '/pulls', '/projects', '/actions',
        '/security', '/pulse', '/graphs', '/search', '/marketplace',
        '/explore', '/topics', '/trending', '/sponsors', '/new',
        '/organizations/', '/codespaces'
      ];
      if (nonRepoPaths.some(p => window.location.pathname.includes(p))) {
        // Allow issues/pulls detail pages
        if ((window.location.pathname.includes('/issues/') || window.location.pathname.includes('/pull/')) && pathParts.length > 3) {
          // It's an issue/PR detail page, potentially show button? For now, let's keep it strict to repo main/code view
          // return true; // Uncomment if you want the button on issue/PR details
          return false; // Keep button only on repo views for now
        }
        return false;
      }

      // Check for main repo container elements (more reliable)
      const mainContentSelectors = [
        'main#js-repo-pjax-container',                     // Older structure
        'div[data-pjax="#repo-content-pjax-container"]', // Newer structure
        '.repohead',                                       // Repo header element
        '.repository-content'                              // Main content area
      ];
      if (mainContentSelectors.some(sel => document.querySelector(sel))) {
        return true;
      }


      // Fallback: If path has user/repo and not excluded, assume it's a repo page.
      // Be cautious with this fallback.
      // log("Falling back to path check for repo page detection.");
      // return true;

      return false; // Stricter check - rely on selectors


    } catch (e) {
      errorLog('Error checking if it is a repo page:', e);
      return false;
    }
  }

  /**
   * Extracts username and repository name from the current URL.
   * @returns {{user: string, repo: string} | null} Object with user/repo or null if not found.
   */
  function getUserAndRepo() {
    try {
      const pathParts = window.location.pathname.split('/').filter(part => part.length > 0);
      if (pathParts.length >= 2) {
        // Handle cases like /user/repo/tree/branch or /user/repo/blob/branch/file
        return {
          user: pathParts[0],
          repo: pathParts[1]
        };
      }
    } catch (e) {
      errorLog('Error getting user and repo info:', e);
    }
    return null;
  }

  // --- Button Creation ---
  /**
   * Creates the SVG icon element for the DeepWiki button.
   * @returns {SVGSVGElement} The SVG element.
   */
  function createSVGIconElement() {
    const svgNS = 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(svgNS, 'svg');
    // Using a simpler, cleaner icon representation if possible, or the provided one.
    // Let's use the provided complex one for now.
    svg.setAttribute('class', 'octicon'); // Use GitHub's icon class if possible
    svg.setAttribute('width', '16');
    svg.setAttribute('height', '16');
    svg.setAttribute('viewBox', '110 110 460 500'); // As provided in script 2
    svg.setAttribute('style', 'margin-right: 4px; vertical-align: text-bottom; color: var(--color-accent-fg);'); // Use CSS variable for color
    // Paths from Script 2 (ensure they are valid and display correctly)
    svg.innerHTML = `<path style="fill:#21c19a" d="M418.73,332.37c9.84-5.68,22.07-5.68,31.91,0l25.49,14.71c.82.48,1.69.8,2.58,1.06.19.06.37.11.55.16.87.21,1.76.34,2.65.35.04,0,.08.02.13.02.1,0,.19-.03.29-.04.83-.02,1.64-.13,2.45-.32.14-.03.28-.05.42-.09.87-.24,1.7-.59,2.5-1.03.08-.04.17-.06.25-.1l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-.08.04-.13.11-.2.16-.78.48-1.51,1.02-2.15,1.66-.1.1-.18.21-.28.31-.57.6-1.08,1.26-1.51,1.97-.07.12-.15.22-.22.34-.44.77-.77,1.6-1.03,2.47-.05.19-.1.37-.14.56-.22.89-.37,1.81-.37,2.76v29.43c0,11.36-6.11,21.95-15.95,27.63-9.84,5.68-22.06,5.68-31.91,0l-25.49-14.71c-.82-.48-1.69-.8-2.57-1.06-.19-.06-.37-.11-.56-.16-.88-.21-1.76-.34-2.65-.34-.13,0-.26.02-.4.02-.84.02-1.66.13-2.47.32-.13.03-.27.05-.4.09-.87.24-1.71.6-2.51,1.04-.08.04-.16.06-.24.1l-50.97,29.43c-3.65-2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22l50.97,29.43c.08.04.17.06.24.1.8.44,1.64.79,2.5,1.03.14.04.28.06.42.09.81.19,1.62.3,2.45.32.1,0,.19.04.29.04.04,0,.08-.02.13-.02.89,0,1.77-.13,2.65-.35.19-.04.37-.1.56-.16.88-.26,1.75-.59,2.58-1.06l25.49-14.71c9.84-5.68,22.06-5.68,31.91,0,9.84,5.68,15.95,16.27,15.95,27.63v29.43c0,.95.15,1.87.37,2.76.05.19.09.37.14.56.25.86.59,1.69,1.03,2.47.07.12.15.22.22.34.43.71.94,1.37,1.51,1.97.1.1.18.21.28.31.65.63,1.37,1.18,2.15,1.66.07.04.13.11.2.16l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-.08-.04-.16-.06-.24-.1-.8-.44-1.64-.8-2.51-1.04-.13-.04-.26-.05-.39-.09-.82-.2-1.65-.31-2.49-.33-.13,0-.25-.02-.38-.02-.89,0-1.78.13-2.66.35-.18.04-.36.1-.54.15-.88.26-1.75.59-2.58,1.07l-25.49,14.72c-9.84,5.68-22.07,5.68-31.9,0-9.84-5.68-15.95-16.27-15.95-27.63s6.11-21.95,15.95-27.63Z"></path><path style="fill:#3969ca" d="M141.09,317.65l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c.08-.04.13-.11.2-.16.78-.48,1.51-1.02,2.15-1.66.1-.1.18-.21.28-.31.57-.6,1.08-1.26,1.51-1.97.07-.12.15-.22.22-.34.44-.77.77-1.6,1.03-2.47.05-.19.1-.37.14-.56.22-.89.37-1.81.37-2.76v-29.43c0-11.36,6.11-21.95,15.96-27.63s22.06-5.68,31.91,0l25.49,14.71c.82.48,1.69.8,2.57,1.06.19.06.37.11.56.16.87.21,1.76.34,2.64.35.04,0,.09.02.13.02.1,0,.19-.04.29-.04.83-.02,1.65-.13,2.45-.32.14-.03.28-.05.41-.09.87-.24,1.71-.6,2.51-1.04.08-.04.16-.06.24-.1l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-.08.04-.13.11-.2.16-.78.48-1.51,1.02-2.15,1.66-.1.1-.18.21-.28.31-.57.6-1.08,1.26-1.51,1.97-.07.12-.15.22-.22.34-.44.77-.77,1.6-1.03,2.47-.05.19-.1.37-.14.56-.22.89-.37,1.81-.37,2.76v29.43c0,11.36-6.11,21.95-15.95,27.63-9.84,5.68-22.07,5.68-31.91,0l-25.49-14.71c-.82-.48-1.69-.8-2.58-1.06-.19-.06-.37-.11-.55-.16-.88-.21-1.76-.34-2.65-.35-.13,0-.26.02-.4.02-.83.02-1.66.13-2.47.32-.13.03-.27.05-.4.09-.87.24-1.71.6-2.51,1.04-.08.04-.16.06-.24.1l-50.97,29.43c-3.65-2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22Z"></path><path style="fill:#0294de" d="M396.88,484.35l-50.97-29.43c-.08-.04-.17-.06-.24-.1-.8-.44-1.64-.79-2.51-1.03-.14-.04-.27-.06-.41-.09-.81-.19-1.64-.3-2.47-.32-.13,0-.26-.02-.39-.02-.89,0-1.78.13-2.66.35-.18.04-.36.1-.54.15-.88.26-1.76.59-2.58,1.07l-25.49,14.72c-9.84,5.68-22.06,5.68-31.9,0-9.84-5.68-15.96-16.27-15.96-27.63v-29.43c0-.95-.15-1.87-.37-2.76-.05-.19-.09-.37-.14-.56-.25-.86-.59-1.69-1.03-2.47-.07-.12-.15.22-.22.34-.43-.71-.94-1.37-1.51-1.97-.1-.1-.18-.21-.28-.31-.65-.63-1.37-1.18-2.15-1.66-.07-.04-.13-.11-.2-.16l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-3.65,2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22l50.97,29.43c.08.04.17.06.25.1.8.44,1.63.79,2.5,1.03.14.04.29.06.43.09.8.19,1.61.3,2.43.32.1,0,.2.04.3.04.04,0,.09-.02.13-.02.88,0,1.77-.13,2.64-.34.19-.04.37-.1.56-.16.88-.26,1.75-.59,2.57-1.06l25.49-14.71c9.84-5.68,22.06-5.68,31.91,0,9.84,5.68,15.95,16.27,15.95,27.63v29.43c0,.95.15,1.87.37,2.76.05.19.09.37.14.56.25.86.59,1.69,1.03,2.47.07.12.15.22.22.34.43.71.94,1.37,1.51,1.97.1.1.18.21.28.31.65.63,1.37,1.18,2.15,1.66.07.04.13.11.2.16l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22Z"></path>`;
    return svg;
  }

  /**
   * Creates the DeepWiki button element.
   * @param {string} user - GitHub username.
   * @param {string} repo - GitHub repository name.
   * @returns {HTMLAnchorElement | null} The button element or null on error.
   */
  function createDeepWikiButton(user, repo) {
    try {
      const deepwikiUrl = `https://deepwiki.com/${user}/${repo}`;

      // Find a template button to mimic style (prefer action buttons)
      const selectors = [
        '.BtnGroup button',              // Button group (e.g., Watch/Fork/Star)
        '.BtnGroup a',                   // Sometimes links are used in groups
        'a.btn',                         // General buttons
        'button.btn',
        '[data-hotkey="w"]',             // Watch button hotkey
        '[data-hotkey="s"]',             // Star button hotkey
        '[data-ga-click*="Fork"]'        // Fork button analytics attrib
      ];
      let templateButton = null;
      for (const selector of selectors) {
        // Find a visible button that isn't the deepwiki one itself
        templateButton = Array.from(document.querySelectorAll(selector)).find(btn =>
          btn.offsetParent !== null && !btn.id.startsWith('deepwiki-button') && !btn.closest(`#${BUTTON_ID}`)
        );
        if (templateButton) break;
      }


      // Create button element
      const button = document.createElement('a');
      button.href = deepwikiUrl;
      button.id = BUTTON_ID;
      button.target = '_blank';
      button.rel = 'noopener noreferrer';
      button.title = `View Wiki for ${user}/${repo} on DeepWiki`;
      button.setAttribute('data-user', user);
      button.setAttribute('data-repo', repo);
      button.setAttribute('aria-label', `View Wiki for ${user}/${repo} on DeepWiki`);
      button.style.textDecoration = 'none'; // Ensure no underline

      // Apply styles
      if (templateButton) {
        // Mimic classes, filtering out state-specific ones
        const classNames = Array.from(templateButton.classList).filter(cls =>
          !['selected', 'disabled', 'tooltipped', 'js-selected-navigation-item'].includes(cls) &&
          !cls.startsWith('BtnGroup') // Avoid BtnGroup item specific styles if adding outside a group
        );
        // Ensure basic button classes are present if missed
        if (!classNames.some(cls => cls.startsWith('btn') || cls.startsWith('Btn'))) {
          classNames.push('btn'); // Add basic button class
        }
        // Add size class if template had one (e.g., btn-sm)
        if (Array.from(templateButton.classList).some(cls => cls.includes('-sm'))) {
          if (!classNames.includes('btn-sm')) classNames.push('btn-sm');
        } else if (Array.from(templateButton.classList).some(cls => cls.includes('-large'))) {
          if (!classNames.includes('btn-large')) classNames.push('btn-large');
        }


        button.className = classNames.join(' ');
        log('Mimicking styles from:', templateButton, 'Resulting classes:', button.className);


      } else {
        // Fallback basic styling if no template found
        log('No template button found, applying fallback styles.');
        button.className = 'btn btn-sm'; // Default to small button
        // Optional: Add minimal inline styles if needed, but prefer classes
        // button.style.backgroundColor = '#f6f8fa';
        // button.style.border = '1px solid rgba(27,31,36,0.15)';
        // button.style.borderRadius = '6px';
        // button.style.color = '#24292f';
        // button.style.padding = '3px 12px';
        // button.style.fontSize = '12px';
        // button.style.fontWeight = '500';
        // button.style.lineHeight = '20px';
      }


      // Add SVG icon (inside the button)
      const svgIcon = createSVGIconElement();
      button.appendChild(svgIcon);

      // Add text (inside the button)
      const text = document.createTextNode(' DeepWiki'); // Add space for clarity
      button.appendChild(text);


      // Add click tracking/logging
      button.addEventListener('click', function (e) {
        log(`DeepWiki button clicked for: ${user}/${repo}`);
        // Optional: Add analytics tracking here if desired
      });

      return button;
    } catch (e) {
      errorLog('Error creating DeepWiki button:', e);
      return null;
    }
  }

  // --- Button Insertion ---
  /**
   * Finds the best location and inserts the DeepWiki button.
   */
  function addDeepWikiButton() {
    // 1. Pre-checks
    if (!isRepoPage()) {
      // log('Not a repository page, skipping button add.');
      return;
    }
    if (document.getElementById(BUTTON_ID)) {
      // log('DeepWiki button already exists.');
      return;
    }

    const userAndRepo = getUserAndRepo();
    if (!userAndRepo) {
      errorLog('Could not extract user/repo info, cannot add button.');
      return;
    }

    // 2. Create the button
    const deepWikiButton = createDeepWikiButton(userAndRepo.user, userAndRepo.repo);
    if (!deepWikiButton) {
      errorLog('Button creation failed.');
      return;
    }

    // 3. Find insertion point (try multiple locations)
    // Prioritize the specific target: repository-details-container div's ul
    const targetSelectors = [
      // --- Primary Target (Specific container requested) ---
      '#repository-details-container ul',     // Specific container requested
      // --- Secondary Targets (UL elements for list items) ---
      '.pagehead-actions ul',                 // Older structure action list
      '.AppHeader-context-full nav > ul',      // Newest header nav
      'nav[aria-label="Repository"] ul',      // Repo navigation tabs
      // --- Fallback Targets (Other areas) ---
      '.gh-header-actions',                   // Newer structure action area
      '.repository-content .Box-header .d-flex .BtnGroup',
      '#repository-container-header .BtnGroup',
      '.file-navigation',                     // File browser header
      '.Layout-sidebar'                       // Right sidebar
    ];

    let targetElement = null;
    let insertionMethod = 'appendChild'; // Default: add to the end

    for (const selector of targetSelectors) {
      targetElement = document.querySelector(selector);
      if (targetElement) {
        log(`Found target element using selector: ${selector}, tagName: ${targetElement.tagName}`);
        // If we found the specific target we're looking for, break immediately
        if (selector === '#repository-details-container ul') {
          log('Found the specific target container requested');
        }
        break; // Stop searching once a target is found
      }
    }


    // 4. Insert the button
    if (targetElement) {
      try {
        let elementToInsert;

        // If target is a UL, always add as a list item
        if (targetElement.tagName === 'UL') {
          log('Target is a UL element, creating list item for DeepWiki button');
          const li = document.createElement('li');
          // Try to copy classes from sibling LIs for better alignment/styling in lists
          const siblingLi = targetElement.querySelector('li:last-child');
          if (siblingLi) {
            li.className = siblingLi.className;
            log(`Copied classes from sibling LI: ${siblingLi.className}`);
          }
          li.style.marginLeft = '8px'; // Ensure some space if classes don't provide it
          li.appendChild(deepWikiButton);
          elementToInsert = li;
        } else if (targetElement.classList.contains('BtnGroup')) {
          // If inserting into a button group, don't wrap, just append
          elementToInsert = deepWikiButton;
        } else {
          // For any other target, add directly
          elementToInsert = deepWikiButton;
        }

        // Insert the element
        targetElement.appendChild(elementToInsert);

        log(`Successfully added DeepWiki button for ${userAndRepo.user}/${userAndRepo.repo}`);
        if (targetElement.closest('#repository-details-container')) {
          log('DeepWiki button was added to the repository-details-container as requested');
        }
      } catch (e) {
        errorLog('Error inserting button into target element:', e, targetElement);
      }
    } else {
      log('Could not find a suitable location to add the DeepWiki button.');
    }
  }

  // --- Dynamic Loading Handling ---
  let observer = null;
  let mutationDebounceTimeout = null;

  /**
   * Sets up a MutationObserver to watch for DOM changes and URL changes (via DOM).
   */
  function setupObserver() {
    if (observer) {
      // log("Observer already running.");
      return; // Don't set up multiple observers
    }

    try {
      observer = new MutationObserver((mutations) => {
        // Check if URL changed significantly (indicating SPA navigation)
        if (location.href !== lastUrl) {
          log(`URL changed from ${lastUrl} to ${location.href}`);
          lastUrl = location.href;
          // Clear any existing button immediately on URL change before adding new one
          const existingButton = document.getElementById(BUTTON_ID);
          if (existingButton) {
            existingButton.remove();
            log('Removed old button due to URL change.');
          }

          // Re-run add button logic after a short delay for the page to settle
          clearTimeout(mutationDebounceTimeout); // Clear previous debounce timer
          mutationDebounceTimeout = setTimeout(addDeepWikiButton, 300); // Debounce checks
        } else {
          // URL didn't change, but DOM did. Check if button *should* be there but isn't.
          // This handles cases where parts of the header re-render without full navigation.
          if (isRepoPage() && !document.getElementById(BUTTON_ID)) {
            clearTimeout(mutationDebounceTimeout); // Clear previous debounce timer
            mutationDebounceTimeout = setTimeout(addDeepWikiButton, 500); // Longer debounce for general mutations
          }
        }

        // Optimization: Disconnect observer if we are definitely not on a repo page?
        // Could add: if (!isRepoPage() && observer) { observer.disconnect(); observer = null; log('Disconnected observer, not repo page'); }
        // But re-connecting might be tricky, so let's keep it simple for now.
      });

      observer.observe(document.body, {
        childList: true, // Watch for adding/removing nodes
        subtree: true    // Watch descendants too
      });
      log('MutationObserver set up successfully.');
    } catch (e) {
      errorLog('Failed to set up MutationObserver:', e);
    }
  }

  // --- Initialization ---
  /**
   * Initializes the script: adds the button and sets up the observer.
   */
  function init() {
    log('Initializing DeepWiki Button script...');

    // Initial attempt to add the button
    addDeepWikiButton();

    // Retry mechanism for initial load race conditions
    // Use increasing delays
    setTimeout(addDeepWikiButton, 500);
    setTimeout(addDeepWikiButton, 1500); // Longer delay
    setTimeout(addDeepWikiButton, 3000); // Even longer


    // Set up the observer to handle SPA navigation and dynamic content
    setupObserver();

    // Optional: Listen to popstate for back/forward navigation (though observer often catches this too)
    window.addEventListener('popstate', () => {
      log('popstate event detected');
      // Give observer a chance first, but force check after a delay
      setTimeout(addDeepWikiButton, 200);
    });

  }

  // --- Run ---
  // Use DOMContentLoaded for initial run, but also check readyState for already loaded pages.
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    // The document is already loaded ('interactive' or 'complete')
    init();
  }

})();