您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download selected files and folders from GitHub repositories.
- // ==UserScript==
- // @name GitZip Lite
- // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
- // @namespace https://github.com/tizee-tampermonkey-scripts/tampermonkey-gitzip-lite
- // @version 1.6.3
- // @description Download selected files and folders from GitHub repositories.
- // @author tizee
- // @match https://github.com/*/*
- // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
- // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
- // @require https://unpkg.com/powerglitch@2.4.0/dist/powerglitch.min.js
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_addStyle
- // @grant GM_registerMenuCommand
- // @connect api.github.com
- // @connect raw.githubusercontent.com
- // @run-at document-end
- // @license MIT
- // ==/UserScript==
- (function () {
- "use strict";
- const itemCollectSelector =
- "div.js-navigation-item, table tbody tr.react-directory-row > td[class$='cell-large-screen']";
- const tokenKey = "githubApiToken";
- const { parseRepoURL, getGitURL, getInfoURL } = {
- parseRepoURL: (repoUrl) => {
- const repoExp = new RegExp(
- "^https://github.com/([^/]+)/([^/]+)(/(tree|blob)/([^/]+)(/(.*))?)?"
- );
- const matches = repoUrl.match(repoExp);
- if (!matches || matches.length === 0) return null;
- const author = matches[1];
- const project = matches[2];
- const branch = matches[5];
- const type = matches[4];
- const path = matches[7] || "";
- const rootUrl = branch
- ? `https://github.com/${author}/${project}/tree/${branch}`
- : `https://github.com/${author}/${project}`;
- if (!type && repoUrl.length - rootUrl.length > 1) {
- return null;
- }
- return {
- author,
- project,
- branch,
- type,
- path,
- inputUrl: repoUrl,
- rootUrl,
- };
- },
- getGitURL: (author, project, type, sha) => {
- if (type === "blob" || type === "tree") {
- const pluralType = type + "s";
- return `https://api.github.com/repos/${author}/${project}/git/${pluralType}/${sha}`;
- }
- return null;
- },
- getInfoURL: (author, project, path, branch) => {
- let url = `https://api.github.com/repos/${author}/${project}/contents/${path}`;
- if (branch) {
- url += `?ref=${branch}`;
- }
- return url;
- },
- };
- // --- GitZip Functions ---
- function base64toBlob(base64Data, contentType) {
- contentType = contentType || "";
- const sliceSize = 1024;
- const byteCharacters = atob(base64Data);
- const bytesLength = byteCharacters.length;
- const slicesCount = Math.ceil(bytesLength / sliceSize);
- const byteArrays = new Array(slicesCount);
- for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
- const begin = sliceIndex * sliceSize;
- const end = Math.min(begin + sliceSize, bytesLength);
- const bytes = new Array(end - begin);
- for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
- bytes[i] = byteCharacters[offset].charCodeAt(0);
- }
- byteArrays[sliceIndex] = new Uint8Array(bytes);
- }
- return new Blob(byteArrays, { type: contentType });
- }
- function callAjax(url, token) {
- return new Promise(function (resolve, reject) {
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- headers: {
- Authorization: token ? "token " + token : undefined,
- Accept: "application/json",
- },
- onload: function (response) {
- if (response.status >= 200 && response.status < 300) {
- try {
- const jsonResponse = JSON.parse(response.responseText);
- resolve({ response: jsonResponse });
- } catch (e) {
- console.debug("Error parsing JSON:", e);
- reject(e);
- }
- } else {
- console.debug("Request failed with status:", response.status);
- logMessage("ERROR", `Request failed with status: ${response.status}`);
- reject(response);
- }
- },
- onerror: function (error) {
- logMessage("ERROR", error);
- reject(error);
- },
- });
- });
- }
- // New dedicated function for binary downloads
- function downloadFile(url, token) {
- return new Promise(function (resolve, reject) {
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- responseType: "arraybuffer",
- headers: {
- Authorization: token ? "token " + token : undefined,
- Accept: "application/octet-stream",
- },
- onload: function (response) {
- if (response.status >= 200 && response.status < 300) {
- resolve(new Uint8Array(response.response));
- } else {
- reject(new Error(`Download failed: ${response.status}`));
- }
- },
- onerror: reject,
- });
- });
- }
- // --- End GitZip Functions ---
- function addCheckboxes() {
- const fileRows = document.querySelectorAll(itemCollectSelector);
- fileRows.forEach((row) => {
- if (row.querySelector(".gitziplite-check-wrap")) return;
- // Ensure the row is relatively positioned
- row.style.position = "relative";
- const checkboxContainer = document.createElement("div");
- checkboxContainer.classList.add("gitziplite-check-wrap");
- checkboxContainer.style.position = "absolute";
- checkboxContainer.style.left = "4px";
- checkboxContainer.style.top = "50%";
- checkboxContainer.style.transform = "translateY(-50%)";
- checkboxContainer.style.display = "flex";
- checkboxContainer.style.alignItems = "center";
- checkboxContainer.style.height = "100%";
- checkboxContainer.style.display = "none";
- const checkbox = document.createElement("input");
- checkbox.type = "checkbox";
- checkbox.classList.add("gitziplite-checkbox");
- checkboxContainer.appendChild(checkbox);
- // Find the first element to insert before. Handles both file and directory rows.
- const insertBeforeElement = row.firstChild;
- if (insertBeforeElement) {
- row.insertBefore(checkboxContainer, insertBeforeElement);
- } else {
- row.appendChild(checkboxContainer); // Fallback if no children exist
- }
- // Add event listeners for hover
- row.addEventListener("mouseenter", () => {
- checkboxContainer.style.display = "flex";
- });
- row.addEventListener("mouseleave", () => {
- if (!checkbox.checked) {
- checkboxContainer.style.display = "none";
- }
- });
- row.addEventListener("dblclick", () => {
- console.debug("double click", row, checkbox);
- if (checkbox.checked) {
- checkboxContainer.style.display = "none";
- } else {
- checkboxContainer.style.display = "flex";
- }
- checkbox.checked = !checkbox.checked;
- checkbox.dispatchEvent(new Event("change"));
- });
- // Add event listener for checkbox change
- checkbox.addEventListener("change", () => {
- let link;
- if (row.tagName === "TD") {
- link = row.querySelector("a[href]");
- } else {
- link = row.querySelector("a[href]");
- }
- if (link) {
- const title = link.textContent.trim();
- const command = checkbox.checked ? "SELECT" : "UNSELECT";
- logMessage(command, title);
- }
- });
- });
- }
- let logWindow;
- let logToggleButton;
- let downloadButton;
- let mainContainer;
- let stickerButton;
- // Add global styles
- GM_addStyle(`
- /* Container Styles */
- .gitziplite-container {
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- z-index: 1000;
- width: 480px;
- background-color: rgba(28, 28, 30, 0.95);
- border-radius: 16px;
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
- padding: 1.25rem;
- backdrop-filter: blur(20px);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
- border: 1px solid rgba(255, 255, 255, 0.08);
- display: none; /* Hide window by default */
- }
- /* Sidebar sticker button */
- .gitziplite-sticker-button {
- position: fixed;
- right: 0;
- top: 30%;
- background-color: rgba(28, 28, 30, 0.95);
- color: white;
- border-radius: 8px 0 0 8px;
- padding: 10px;
- cursor: pointer;
- z-index: 999;
- box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
- transition: all 0.2s ease;
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-right: none;
- }
- .gitziplite-sticker-button:hover {
- background-color: rgba(40, 40, 45, 0.95);
- transform: translateX(-2px);
- }
- /* Hide button for the container */
- .gitziplite-hide-button {
- position: absolute;
- top: -14px;
- right: -14px;
- width: 28px;
- height: 28px;
- border-radius: 14px;
- background-color: rgba(28, 28, 30, 0.95);
- border: 1px solid rgba(255, 255, 255, 0.08);
- color: white;
- font-size: 16px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background-color 0.2s ease;
- z-index: 1001;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- }
- .gitziplite-hide-button:hover {
- background-color: rgba(40, 40, 45, 0.95);
- }
- /* Log Window Styles */
- .gitziplite-log {
- width: 100%;
- height: 16rem;
- margin-bottom: 0.75rem;
- overflow-y: auto;
- border-radius: 12px;
- background-color: rgba(0, 0, 0, 0.25);
- color: #E4E4E4;
- font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
- font-size: 12px;
- line-height: 1.5;
- padding: 0.75rem;
- scrollbar-width: thin;
- scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
- border: 1px solid rgba(255, 255, 255, 0.06);
- }
- /* Scrollbar Styles */
- .gitziplite-log::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
- .gitziplite-log::-webkit-scrollbar-track {
- background: transparent;
- }
- .gitziplite-log::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.2);
- border-radius: 3px;
- }
- .gitziplite-log::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.3);
- }
- /* Log Entry Styles */
- .gitziplite-log-entry {
- padding: 0.25rem 0;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- opacity: 0;
- transform: translateY(10px);
- animation: gitziplite-fadeIn 0.2s ease-out forwards;
- }
- .gitziplite-log-timestamp {
- color: #8E8E93;
- min-width: 5.5rem;
- font-feature-settings: "tnum";
- font-variant-numeric: tabular-nums;
- }
- .gitziplite-log-command {
- min-width: 5rem;
- padding: 0.125rem 0.5rem;
- border-radius: 6px;
- font-weight: 500;
- text-align: center;
- backdrop-filter: blur(8px);
- }
- .gitziplite-log-content {
- color: #E4E4E4;
- flex: 1;
- }
- /* Button Container */
- .gitziplite-buttons {
- display: flex;
- gap: 0.75rem;
- justify-content: space-between;
- align-items: center;
- }
- /* Button Styles */
- .gitziplite-button {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
- font-size: 13px;
- font-weight: 510;
- padding: 0.625rem 1rem;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- border: none;
- outline: none;
- white-space: nowrap;
- user-select: none;
- position: relative;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
- }
- .gitziplite-button-primary {
- background-color: #0A84FF;
- color: white;
- }
- .gitziplite-button-primary:hover {
- background-color: #007AFF;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(10, 132, 255, 0.3);
- }
- .gitziplite-button-primary:active {
- transform: translateY(0);
- background-color: #0062CC;
- box-shadow: 0 1px 2px rgba(10, 132, 255, 0.2);
- }
- .gitziplite-button-secondary {
- background-color: rgba(255, 255, 255, 0.1);
- color: #FFFFFF;
- border: 1px solid rgba(255, 255, 255, 0.1);
- }
- .gitziplite-button-secondary:hover {
- background-color: rgba(255, 255, 255, 0.15);
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- .gitziplite-button-secondary:active {
- transform: translateY(0);
- background-color: rgba(255, 255, 255, 0.05);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
- }
- /* Animation */
- @keyframes gitziplite-fadeIn {
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
- `);
- function createDownloadButton() {
- // Create sticker button for the sidebar
- stickerButton = document.createElement("div");
- stickerButton.className = "gitziplite-sticker-button";
- stickerButton.innerHTML = `
- <div style="display: flex; flex-direction: column; align-items: center;">
- <svg width="16" height="16" viewBox="0 0 16 16" style="margin-bottom: 8px;">
- <path fill="currentColor" d="M8 12l-4.5-4.5 1.5-1.5L7 8.25V2h2v6.25L11 6l1.5 1.5L8 12zm-6 2v-2h12v2H2z"></path>
- </svg>
- <div style="writing-mode: vertical-lr; transform: rotate(180deg); font-size: 12px; letter-spacing: 1px; margin-top: 5px;">GitZip</div>
- </div>
- `;
- stickerButton.setAttribute("title", "Show GitZip Download Window");
- stickerButton.addEventListener("click", () => {
- mainContainer.style.display = "block";
- stickerButton.style.display = "none";
- });
- document.body.appendChild(stickerButton);
- // Main container
- mainContainer = document.createElement("div");
- mainContainer.className = "gitziplite-container";
- // Hide button
- const hideButton = document.createElement("button");
- hideButton.className = "gitziplite-hide-button";
- hideButton.innerHTML = "✕";
- hideButton.setAttribute("title", "Hide Download Window");
- hideButton.addEventListener("click", () => {
- mainContainer.style.display = "none";
- stickerButton.style.display = "block";
- });
- mainContainer.appendChild(hideButton);
- // Log Window Container
- logWindow = document.createElement("div");
- logWindow.setAttribute("aria-label", "Log Window");
- logWindow.className = "gitziplite-log";
- logWindow.style.display = "none";
- // Button Container
- const buttonContainer = document.createElement("div");
- buttonContainer.className = "gitziplite-buttons";
- // Log Toggle Button
- logToggleButton = document.createElement("button");
- logToggleButton.textContent = "Show Log";
- logToggleButton.className = "gitziplite-button gitziplite-button-secondary";
- logToggleButton.addEventListener("click", () => {
- logWindow.style.display =
- logWindow.style.display === "none" ? "block" : "none";
- logToggleButton.textContent =
- logWindow.style.display === "none" ? "Show Log" : "Hide Log";
- });
- // Download Button
- downloadButton = document.createElement("button");
- downloadButton.textContent = "Download Selected";
- downloadButton.className = "gitziplite-button gitziplite-button-primary";
- downloadButton.addEventListener("click", downloadSelected);
- // Assemble the UI
- buttonContainer.appendChild(logToggleButton);
- buttonContainer.appendChild(downloadButton);
- mainContainer.appendChild(logWindow);
- mainContainer.appendChild(buttonContainer);
- document.body.appendChild(mainContainer);
- // Hide the window by default
- mainContainer.style.display = "none";
- stickerButton.style.display = "block";
- }
- function logMessage(command, content) {
- const now = new Date();
- const timestamp = `${String(now.getHours()).padStart(2, "0")}:${String(
- now.getMinutes()
- ).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
- const commandColors = {
- ERROR: { bg: "#FF453A20", color: "#FF453A" },
- SUCCESS: { bg: "#32D74B20", color: "#32D74B" },
- PROCESS: { bg: "#0A84FF20", color: "#0A84FF" },
- SELECT: { bg: "#FFD60A20", color: "#FFD60A" },
- UNSELECT: { bg: "#FFD60A20", color: "#FFD60A" },
- INFO: { bg: "#64D2FF20", color: "#64D2FF" },
- };
- const colorScheme =
- commandColors[command.toUpperCase()] || commandColors.INFO;
- const logEntry = document.createElement("div");
- logEntry.className = "gitziplite-log-entry";
- logEntry.innerHTML = `
- <span class="gitziplite-log-timestamp">${timestamp}</span>
- <span class="gitziplite-log-command" style="background: ${colorScheme.bg}; color: ${colorScheme.color}">
- ${command}
- </span>
- <span class="gitziplite-log-content">${content}</span>
- `;
- logWindow.appendChild(logEntry);
- logWindow.scrollTop = logWindow.scrollHeight;
- }
- /**
- * Collects selected files and folders from the DOM.
- * @returns {{files: [], folders: []}} - An object containing arrays of selected files and folders.
- */
- function collectSelectedItems() {
- const selectedFiles = [];
- const selectedFolders = [];
- const checkboxes = document.querySelectorAll(
- ".gitziplite-checkbox:checked"
- );
- checkboxes.forEach((checkbox) => {
- const row = checkbox.parentNode.parentNode; // Direct parent access
- if (!row) {
- console.warn("Could not find a parent row for a selected checkbox.");
- return; // Skip to the next checkbox
- }
- console.debug(row);
- let link;
- if (row.tagName === "TD") {
- link = row.querySelector("a[href]");
- } else {
- link = row.querySelector("a[href]");
- }
- if (link) {
- const href = link.href;
- const title = link.textContent.trim();
- const resolved = parseRepoURL(href);
- if (resolved && resolved.type === "blob") {
- selectedFiles.push({ href: href, title: title });
- } else if (resolved && resolved.type === "tree") {
- selectedFolders.push({ href: href, title: title });
- }
- }
- });
- return { files: selectedFiles, folders: selectedFolders };
- }
- /**
- * Zips the given contents and triggers a download.
- * @param {Array<{path: string, content: string}>} allContents - Array of file contents to zip.
- * @param {object} resolvedUrl - Parsed URL information of the repository.
- */
- function zipAndDownload(allContents, resolvedUrl) {
- if (allContents.length === 1) {
- // Handle single file download
- const singleItem = allContents[0];
- console.debug(singleItem);
- if (singleItem.isBinary) {
- // Create Blob directly from Uint8Array
- const blob = new Blob([singleItem.content], {
- type: "application/octet-stream",
- });
- saveAs(blob, singleItem.path);
- } else {
- // Handle base64 encoded text files
- const blob = base64toBlob(singleItem.content, "");
- saveAs(blob, singleItem.path);
- }
- } else {
- // Handle zip archive creation
- try {
- const currDate = new Date();
- const dateWithOffset = new Date(
- currDate.getTime() - currDate.getTimezoneOffset() * 60000
- );
- window.JSZip.defaults.date = dateWithOffset;
- const zip = new window.JSZip();
- allContents.forEach((item) => {
- if (item.isBinary) {
- // Add binary file as Uint8Array
- zip.file(item.path, item.content, {
- createFolders: true,
- binary: true,
- date: dateWithOffset,
- });
- } else {
- // Add base64 encoded file
- zip.file(item.path, item.content, {
- createFolders: true,
- base64: true,
- date: dateWithOffset,
- });
- }
- });
- zip.generateAsync({ type: "blob" }).then((content) => {
- saveAs(
- content,
- [resolvedUrl.project]
- .concat(resolvedUrl.path.split("/"))
- .join("-") + ".zip"
- );
- });
- } catch (error) {
- console.debug("Error zipping files:", error);
- logMessage("ERROR", "zipping files.");
- }
- }
- }
- async function downloadSelected() {
- const { files: selectedFiles, folders: selectedFolders } =
- collectSelectedItems();
- if (selectedFiles.length === 0 && selectedFolders.length === 0) {
- logMessage("ERROR", "No files or folders selected.");
- return;
- }
- const resolvedUrl = parseRepoURL(window.location.href);
- if (!resolvedUrl) {
- logMessage("ERROR", "Could not resolve repository URL.");
- return;
- }
- const githubToken = GM_getValue(tokenKey);
- const allContents = [];
- async function processFolder(folder, pathPrefix = "") {
- logMessage("PROCESS", `${folder.title}`);
- const folderResolvedUrl = parseRepoURL(folder.href);
- const apiUrl = getInfoURL(
- folderResolvedUrl.author,
- folderResolvedUrl.project,
- folderResolvedUrl.path,
- folderResolvedUrl.branch
- );
- try {
- const xmlResponse = await callAjax(apiUrl, githubToken);
- const folderContents = xmlResponse.response;
- for (const item of folderContents) {
- const itemPath = pathPrefix + "/" + item.name;
- if (item.type === "file") {
- logMessage("PROCESS", `${itemPath}`);
- const fileInfoUrl = getInfoURL(
- folderResolvedUrl.author,
- folderResolvedUrl.project,
- folderResolvedUrl.path + "/" + item.name,
- folderResolvedUrl.branch
- );
- const fileXmlResponse = await callAjax(fileInfoUrl, githubToken);
- const fileContent = fileXmlResponse.response;
- allContents.push({
- path: itemPath,
- content: fileContent.content,
- });
- } else if (item.type === "dir") {
- await processFolder(
- { href: folder.href + "/" + item.name, title: item.name },
- itemPath
- );
- }
- }
- } catch (error) {
- console.debug("Error fetching folder:", folder.title, error);
- logMessage("ERROR", `Error fetching folder: ${folder.title}`);
- }
- }
- for (const folder of selectedFolders) {
- await processFolder(folder, folder.title);
- }
- for (const file of selectedFiles) {
- logMessage("PROCESS", `${file.title}`);
- const fileResolvedUrl = parseRepoURL(file.href);
- const infoUrl = getInfoURL(
- fileResolvedUrl.author,
- fileResolvedUrl.project,
- fileResolvedUrl.path,
- fileResolvedUrl.branch
- );
- logMessage("PROCESS", `${infoUrl}`);
- console.debug(`file info url: ${infoUrl}`);
- try {
- const xmlResponse = await callAjax(infoUrl, githubToken);
- const fileContent = xmlResponse.response;
- if (fileContent.encoding === "base64" && fileContent.content) {
- allContents.push({
- path: file.title,
- content: fileContent.content,
- isBinary: false,
- });
- } else if (fileContent.download_url) {
- // Handle binary file with dedicated download function
- const binaryData = await downloadFile(
- fileContent.download_url,
- githubToken
- );
- allContents.push({
- path: file.title,
- content: binaryData,
- isBinary: true,
- });
- }
- } catch (error) {
- console.debug("Error fetching file:", file.title, error);
- logMessage("ERROR", `fetching file: ${file.title}`);
- return;
- }
- }
- zipAndDownload(allContents, resolvedUrl);
- logMessage("SUCCESS", "Download complete.");
- }
- // Register menu command for setting token
- GM_registerMenuCommand("Set GitHub API Token", () => {
- const token = prompt("Enter your GitHub API token:");
- if (token) {
- GM_setValue(tokenKey, token);
- alert("Token saved successfully!");
- }
- });
- function onDomLoaded() {
- addCheckboxes();
- createDownloadButton();
- }
- function onUrlChange() {
- addCheckboxes();
- }
- // Initialize
- onDomLoaded();
- // Glitch Animation
- PowerGlitch.glitch(logToggleButton, {
- playMode: "click",
- timing: {
- duration: 400,
- easing: "ease-in-out",
- },
- shake: {
- velocity: 20,
- amplitudeX: 0,
- amplitudeY: 0.1,
- },
- });
- PowerGlitch.glitch(downloadButton, {
- playMode: "click",
- timing: {
- duration: 400,
- easing: "ease-in-out",
- },
- });
- // Observe GitHub repository page URL changes (e.g., navigating into a new directory)
- const observer = new MutationObserver(onUrlChange);
- observer.observe(document.body, { childList: true, subtree: true });
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址