您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "Download VSIX" button to VSCode Marketplace extension pages, downloading the file as [ExtensionName][Version].vsix. Waits for dynamic content.
当前为
// ==UserScript== // @name VSCode Marketplace VSIX Downloader // @namespace http://tampermonkey.net/ // @version 1.6 // @description Adds a "Download VSIX" button to VSCode Marketplace extension pages, downloading the file as [ExtensionName][Version].vsix. Waits for dynamic content. // @author Your Name/Adapted from Context // @match https://marketplace.visualstudio.com/items?itemName=* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; if (document.getElementById('vsix-downloader-button')) { console.log('VSIX Downloader: Button already exists (initial check).'); return; } const extensionDetails = { version: "", publisher: "", identifier: "", getDownloadUrl: function() { if (!this.identifier || this.identifier.split(".").length < 2) { console.error("VSIX Downloader: Invalid or missing identifier for download URL. Identifier:", this.identifier); return "#error-missing-info-for-url"; } const publisherFromName = this.identifier.split(".")[0]; const extensionNamePart = this.identifier.substring(publisherFromName.length + 1); if (!publisherFromName || !extensionNamePart || !this.version) { console.error("VSIX Downloader: Missing critical info for download URL. Publisher:", publisherFromName, "ExtName:", extensionNamePart, "Version:", this.version, "Full Identifier:", this.identifier); return "#error-missing-info-for-url"; } return [ "https://", publisherFromName, ".gallery.vsassets.io/_apis/public/gallery/publisher/", publisherFromName, "/extension/", extensionNamePart, "/", this.version, "/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage" ].join(""); }, getFileName: function() { if (!this.identifier || !this.version || !this.identifier.includes('.')) { console.error("VSIX Downloader: Missing critical info or invalid identifier for filename:", this); return "error_unknown_extension.vsix"; } // Extract extension name (part after the first dot in identifier) const extensionName = this.identifier.substring(this.identifier.indexOf('.') + 1); // Format: [title][version].vsix // Sanitize parts to remove characters that might be problematic in filenames, // though typical extension names and versions are usually fine. // For this version, we'll concatenate directly as requested. // Consider adding sanitization if issues arise with specific extension names/versions. const cleanExtensionName = extensionName.replace(/[^a-zA-Z0-9_.-]/g, '_'); // Basic sanitization const cleanVersion = this.version.replace(/[^a-zA-Z0-9_.-]/g, '_'); // Basic sanitization return `${cleanExtensionName}${cleanVersion}.vsix`; }, getDownloadButton: function() { var button = document.createElement("a"); button.id = "vsix-downloader-button"; button.innerHTML = "Download VSIX"; button.href = "javascript:void(0);"; // Will be handled by onclick button.style.fontFamily = "wf_segoe-ui, Helvetica Neue, Helvetica, Arial, Verdana"; button.style.display = "inline-block"; button.style.padding = "4px 8px"; button.style.background = "darkgreen"; button.style.color = "white"; button.style.fontWeight = "bold"; button.style.margin = "5px"; button.style.borderRadius = "3px"; button.style.textDecoration = "none"; const downloadUrl = this.getDownloadUrl(); const fileName = this.getFileName(); if (downloadUrl === "#error-missing-info-for-url" || fileName === "error_unknown_extension.vsix") { button.innerHTML = "Error: Info Missing"; button.style.background = "darkred"; button.title = "Could not determine download URL or filename. Required information is missing or invalid."; button.onclick = (e) => { e.preventDefault(); alert("VSIX Downloader Error: Information to construct download URL or filename is missing. The page might not have fully loaded or there's an issue fetching extension details."); }; return button; } button.setAttribute("data-url", downloadUrl); button.setAttribute("data-filename", fileName); button.title = `Download ${fileName}`; button.onclick = function(event) { event.preventDefault(); const clickedButton = event.target.closest('a'); // Ensure we get the button itself const effectiveDownloadUrl = clickedButton.getAttribute("data-url"); const effectiveFileName = clickedButton.getAttribute("data-filename"); if (effectiveDownloadUrl === "#error-missing-info-for-url") { clickedButton.innerHTML = "Error: Info Missing"; clickedButton.style.background = "darkred"; alert("VSIX Downloader Error: Information to construct download URL is missing (re-check)."); return; } // Store original click handler to restore it later if (!clickedButton.hasOwnProperty('originalOnClick')) { clickedButton.originalOnClick = clickedButton.onclick; } clickedButton.onclick = null; // Disable button during download clickedButton.innerHTML = "Downloading VSIX..."; clickedButton.style.background = "darkorange"; var xhr = new XMLHttpRequest(); console.log("VSIX Downloader: Attempting to download from:", effectiveDownloadUrl, "as", effectiveFileName); xhr.open("GET", effectiveDownloadUrl, true); xhr.responseType = "blob"; xhr.onprogress = function(progressEvent) { if (progressEvent.lengthComputable) { var percentComplete = (progressEvent.loaded / progressEvent.total * 100).toFixed(0); clickedButton.innerHTML = "Downloading VSIX... " + percentComplete + "%"; } }; xhr.onload = function() { if (this.status === 200) { var blobResponse = this.response; var downloadLink = document.createElement("a"); downloadLink.href = window.URL.createObjectURL(blobResponse); downloadLink.download = effectiveFileName; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); window.URL.revokeObjectURL(downloadLink.href); clickedButton.innerHTML = "Download VSIX"; // Reset clickedButton.style.background = "darkgreen"; if (clickedButton.originalOnClick) { clickedButton.onclick = clickedButton.originalOnClick; } else { // Fallback if originalOnClick somehow not set const currentButtonRef = document.getElementById("vsix-downloader-button"); if(currentButtonRef) currentButtonRef.onclick = extensionDetails.getDownloadButton().onclick; // Re-assign a fresh handler } } else { clickedButton.innerHTML = "Error " + this.status + ". Retry?"; clickedButton.style.background = "darkred"; alert("Error " + this.status + " error receiving the document."); if (clickedButton.originalOnClick) clickedButton.onclick = clickedButton.originalOnClick; } }; xhr.onerror = function() { clickedButton.innerHTML = "Network Error. Retry?"; clickedButton.style.background = "darkred"; alert("Error " + xhr.status + " occurred while receiving the document (XHR onerror)."); if (clickedButton.originalOnClick) clickedButton.onclick = clickedButton.originalOnClick; }; xhr.send(); }; return button; } }; const getTextFromAriaLabelledBy = (label) => { const cell = document.querySelector(`td[aria-labelledby='${label}']`); return cell ? cell.innerText.trim() : ""; }; function attemptAndSetup(logVerbose) { extensionDetails.version = ""; extensionDetails.identifier = ""; // --- Data Gathering --- let tempVersion = getTextFromAriaLabelledBy("version"); if (tempVersion) extensionDetails.version = tempVersion; if (logVerbose && extensionDetails.version) console.log("VSIX Downloader: Version from aria-labelledby:", extensionDetails.version); else if (logVerbose && !extensionDetails.version) console.warn("VSIX Downloader: Version not found via aria-labelledby."); let publisherFromTableAria = getTextFromAriaLabelledBy("publisher"); let identifierFromTableAria = getTextFromAriaLabelledBy("identifier"); if (logVerbose && publisherFromTableAria) console.log("VSIX Downloader: Publisher from table (aria-labelledby):", publisherFromTableAria); if (logVerbose && identifierFromTableAria) console.log("VSIX Downloader: Identifier from table (aria-labelledby):", identifierFromTableAria); const urlParams = new URLSearchParams(window.location.search); const itemNameFromUrl = urlParams.get('itemName'); if (itemNameFromUrl) { extensionDetails.identifier = itemNameFromUrl; if (logVerbose) console.log("VSIX Downloader: Identifier from URL (using as primary):", extensionDetails.identifier); if (identifierFromTableAria && identifierFromTableAria !== extensionDetails.identifier && logVerbose) { console.warn(`VSIX Downloader: Identifier from table ('${identifierFromTableAria}') differs from URL ('${extensionDetails.identifier}'). Prioritizing URL version.`); } } else if (identifierFromTableAria) { extensionDetails.identifier = identifierFromTableAria; if (logVerbose) console.log("VSIX Downloader: Identifier from table (aria-labelledby, URL itemName not found):", extensionDetails.identifier); } if (extensionDetails.identifier && extensionDetails.identifier.includes('.')) { extensionDetails.publisher = extensionDetails.identifier.split('.')[0]; if (logVerbose) console.log("VSIX Downloader: Publisher derived from final identifier:", extensionDetails.publisher); if (publisherFromTableAria && publisherFromTableAria !== extensionDetails.publisher && logVerbose) { console.warn(`VSIX Downloader: Publisher from table (aria-labelledby '${publisherFromTableAria}') differs from identifier-derived ('${extensionDetails.publisher}'). Using identifier-derived for download URL.`); } } else if (publisherFromTableAria) { extensionDetails.publisher = publisherFromTableAria; if (logVerbose) console.log("VSIX Downloader: Publisher from table (aria-labelledby, identifier missing or not dot-separated):", extensionDetails.publisher); } if (!extensionDetails.version || !extensionDetails.publisher || !extensionDetails.identifier) { if (logVerbose) console.warn("VSIX Downloader: Critical details missing after specific checks. Attempting broader table scan as fallback."); const metadataTableRows = document.querySelectorAll(".ux-table-metadata tr"); if (metadataTableRows.length > 0) { const keyMappings = { "Version": "version", "Publisher": "publisher", "Unique Identifier": "identifier" }; for (const row of metadataTableRows) { if (row.cells.length === 2) { const keyText = row.cells[0].innerText.trim(); const valueText = row.cells[1].innerText.trim(); if (keyMappings.hasOwnProperty(keyText)) { const detailKey = keyMappings[keyText]; if (!extensionDetails[detailKey] && valueText) { extensionDetails[detailKey] = valueText; if (logVerbose) console.log(`VSIX Downloader: Fallback table scan - found ${detailKey}: ${valueText}`); } } } } if (extensionDetails.identifier && extensionDetails.identifier.includes('.') && (!extensionDetails.publisher || extensionDetails.publisher !== extensionDetails.identifier.split('.')[0])) { extensionDetails.publisher = extensionDetails.identifier.split('.')[0]; if (logVerbose) console.log("VSIX Downloader: Fallback scan - Re-derived publisher from identifier:", extensionDetails.publisher); } } } // --- End Data Gathering --- let missingInfoForSetup = []; if (!extensionDetails.identifier) missingInfoForSetup.push("identifier"); if (!extensionDetails.version) missingInfoForSetup.push("version"); if (extensionDetails.identifier && extensionDetails.identifier.split(".").length < 2) { if (!missingInfoForSetup.includes("identifier")) { missingInfoForSetup.push("identifier format (expected publisher.extensionName)"); } } if (missingInfoForSetup.length > 0) { if (logVerbose) { console.warn("VSIX Downloader: Waiting for critical info: " + missingInfoForSetup.join(", ") + ". Current details:", JSON.stringify(extensionDetails)); } return false; } const potentialDownloadUrl = extensionDetails.getDownloadUrl(); const potentialFileName = extensionDetails.getFileName(); if (potentialDownloadUrl === "#error-missing-info-for-url" || potentialFileName === "error_unknown_extension.vsix") { if (logVerbose) { console.warn("VSIX Downloader: Cannot form valid download URL or filename yet. Details:", JSON.stringify(extensionDetails), "URL:", potentialDownloadUrl, "File:", potentialFileName); } return false; } const buttonInsertionPoint = document.querySelector(".vscode-moreinformation"); let actualInsertionParent = null; let fallbackUsed = false; if (buttonInsertionPoint && buttonInsertionPoint.parentElement) { actualInsertionParent = buttonInsertionPoint.parentElement; } else { const fallbackPoint = document.querySelector(".gallery-banner .control-section") || document.querySelector(".gallery-banner"); if (fallbackPoint) { actualInsertionParent = fallbackPoint; fallbackUsed = true; } } if (!actualInsertionParent) { if (logVerbose) console.warn("VSIX Downloader: Button insertion point not yet available."); return false; } if (document.getElementById('vsix-downloader-button')) { return true; } const downloadButton = extensionDetails.getDownloadButton(); actualInsertionParent.appendChild(downloadButton); if (fallbackUsed) { console.warn("VSIX Downloader: Button added to a fallback location. Details:", JSON.stringify(extensionDetails)); } else { console.log("VSIX Downloader: Button added successfully. Details:", JSON.stringify(extensionDetails)); } return true; } let attempts = 0; const maxAttempts = 30; const retryInterval = 1000; const intervalId = setInterval(() => { attempts++; let logThisAttemptVerbose = (attempts >= maxAttempts); if (document.getElementById('vsix-downloader-button')) { clearInterval(intervalId); return; } if (attemptAndSetup(logThisAttemptVerbose || attempts === 1)) { clearInterval(intervalId); } else if (attempts >= maxAttempts) { clearInterval(intervalId); console.error("VSIX Downloader: Failed to add button after " + maxAttempts + " attempts. Necessary information, a valid download URL/filename, or DOM elements might not be available."); if (!document.getElementById('vsix-downloader-button') && !document.getElementById('vsix-downloader-error-message')) { let errorReason = ""; let missingInfoStrings = []; if (!extensionDetails.version) missingInfoStrings.push("version"); if (!extensionDetails.identifier) { missingInfoStrings.push("identifier"); } else if (extensionDetails.identifier.split(".").length < 2) { if (!missingInfoStrings.includes("identifier")) missingInfoStrings.push("identifier format (publisher.extName)"); } if (missingInfoStrings.length > 0) { errorReason = `Failed to find ${missingInfoStrings.join(" and ")}.`; } else if (extensionDetails.getDownloadUrl() === "#error-missing-info-for-url" || extensionDetails.getFileName() === "error_unknown_extension.vsix") { errorReason = `Found some details, but could not form a valid download URL or filename. (Identifier: '${extensionDetails.identifier}', Version: '${extensionDetails.version}')`; } else { errorReason = `Timed out. Could not find insertion point or other unknown issue.`; } const errorDisplayPoint = document.querySelector(".vscode-moreinformation")?.parentElement || document.querySelector(".gallery-banner") || document.body; if (errorDisplayPoint) { const errorMsgElement = document.createElement('div'); errorMsgElement.id = "vsix-downloader-error-message"; errorMsgElement.textContent = `VSIX Downloader: ${errorReason} Cannot add download button.`; errorMsgElement.style.color = 'red'; errorMsgElement.style.fontWeight = 'bold'; errorMsgElement.style.padding = '10px'; errorMsgElement.style.border = '1px solid red'; errorMsgElement.style.marginTop = '10px'; if (errorDisplayPoint.firstChild && errorDisplayPoint !== document.body) { errorDisplayPoint.insertBefore(errorMsgElement, errorDisplayPoint.firstChild); } else { errorDisplayPoint.appendChild(errorMsgElement); } } } } }, retryInterval); })();