Google Gemini Mod (Toolbar & Download)

Enhances Google Gemini with a toolbar for snippets and canvas content download.

  1. // ==UserScript==
  2. // @name Google Gemini Mod (Toolbar & Download)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.7
  5. // @description Enhances Google Gemini with a toolbar for snippets and canvas content download.
  6. // @description[de] Verbessert Google Gemini mit einer Symbolleiste für Snippets und dem Herunterladen von Canvas-Inhalten.
  7. // @author Adromir
  8. // @match https://gemini.google.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
  10. // @license MIT
  11. // @licenseURL https://opensource.org/licenses/MIT
  12. // @homepageURL https://github.com/adromir/scripts/tree/main/userscripts/gemini-snippets
  13. // @supportURL https://github.com/adromir/scripts/issues
  14. // @grant GM_addStyle
  15. // @grant GM_setClipboard
  16. // @grant GM_getClipboard
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. // ===================================================================================
  23. // I. CONFIGURATION SECTION
  24. // ===================================================================================
  25.  
  26. // --- Customizable Labels for Toolbar Buttons ---
  27. const PASTE_BUTTON_LABEL = "📋 Paste";
  28. const DOWNLOAD_BUTTON_LABEL = "💾 Download Canvas as File";
  29.  
  30. // --- CSS Selectors for DOM Elements ---
  31. const GEMINI_CANVAS_TITLE_TEXT_SELECTOR = "code-immersive-panel > toolbar > div > div.left-panel > h2.title-text.gds-title-s.ng-star-inserted";
  32. // Selector for the "Share" button within the canvas's toolbar area.
  33. const GEMINI_CANVAS_SHARE_BUTTON_SELECTOR = "toolbar div.action-buttons share-button > button";
  34.  
  35. // Selector for the "Copy to Clipboard" button, likely in a modal/overlay after share is clicked.
  36. // Using the more robust alternative focusing on data-test-id if the div structure is too volatile.
  37. const GEMINI_CANVAS_COPY_BUTTON_SELECTOR = "copy-button[data-test-id='copy-button'] > button.copy-button";
  38. // Fallback if the above is too specific or div structure changes often:
  39. // const GEMINI_CANVAS_COPY_BUTTON_SELECTOR_FALLBACK = "body > div:nth-child(n) > div:nth-child(n) > div > div > div > copy-button[data-test-id='copy-button'] > button.copy-button";
  40.  
  41.  
  42. const GEMINI_INPUT_FIELD_SELECTORS = [
  43. '.ql-editor p',
  44. '.ql-editor',
  45. 'div[contenteditable="true"]'
  46. ];
  47.  
  48. // --- Download Feature Configuration ---
  49. const DEFAULT_DOWNLOAD_EXTENSION = "txt";
  50.  
  51. // --- Regular Expressions for Filename Sanitization ---
  52. // eslint-disable-next-line no-control-regex
  53. const INVALID_FILENAME_CHARS_REGEX = /[<>:"/\\|?*\x00-\x1F]/g;
  54. const RESERVED_WINDOWS_NAMES_REGEX = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
  55. const FILENAME_WITH_EXT_REGEX = /^(.+)\.([a-zA-Z0-9]{1,8})$/;
  56. const SUBSTRING_FILENAME_REGEX = /([\w\s.,\-()[\\]{}'!~@#$%^&+=]+?\.([a-zA-Z0-9]{1,8}))(?=\s|$|[,.;:!?])/g;
  57.  
  58. // ===================================================================================
  59. // II. TOOLBAR ELEMENT DEFINITIONS
  60. // ===================================================================================
  61.  
  62. const buttonSnippets = [
  63. { label: "Greeting", text: "Hello Gemini!" },
  64. { label: "Explain", text: "Could you please explain ... in more detail?" },
  65. ];
  66.  
  67. const dropdownConfigurations = [
  68. {
  69. placeholder: "Actions...",
  70. options: [
  71. { label: "Summarize", text: "Please summarize the following text:\n" },
  72. { label: "Ideas", text: "Give me 5 ideas for ..." },
  73. { label: "Code (JS)", text: "Give me a JavaScript code example for ..." },
  74. ]
  75. },
  76. {
  77. placeholder: "Translations",
  78. options: [
  79. { label: "DE -> EN", text: "Translate the following into English:\n" },
  80. { label: "EN -> DE", text: "Translate the following into German:\n" },
  81. { label: "Correct Text", text: "Please correct the grammar and spelling in the following text:\n" }
  82. ]
  83. },
  84. ];
  85.  
  86. // ===================================================================================
  87. // III. SCRIPT LOGIC
  88. // ===================================================================================
  89. const embeddedCSS = `
  90. #gemini-snippet-toolbar-userscript {
  91. position: fixed !important; top: 0 !important; left: 50% !important;
  92. transform: translateX(-50%) !important;
  93. width: auto !important;
  94. max-width: 80% !important;
  95. padding: 10px 15px !important;
  96. z-index: 999999 !important;
  97. display: flex !important; flex-wrap: wrap !important;
  98. gap: 8px !important; align-items: center !important; font-family: 'Roboto', 'Arial', sans-serif !important;
  99. box-sizing: border-box !important; background-color: rgba(40, 42, 44, 0.95) !important;
  100. border-radius: 0 0 16px 16px !important;
  101. box-shadow: 0 4px 12px rgba(0,0,0,0.25);
  102. }
  103. #gemini-snippet-toolbar-userscript button,
  104. #gemini-snippet-toolbar-userscript select {
  105. padding: 4px 10px !important; cursor: pointer !important; background-color: #202122 !important;
  106. color: #e3e3e3 !important; border-radius: 16px !important; font-size: 13px !important;
  107. font-family: inherit !important; font-weight: 500 !important; height: 28px !important;
  108. box-sizing: border-box !important; vertical-align: middle !important;
  109. transition: background-color 0.2s ease, transform 0.1s ease !important;
  110. border: none !important; flex-shrink: 0;
  111. }
  112. #gemini-snippet-toolbar-userscript select {
  113. padding-right: 25px !important;
  114. appearance: none !important;
  115. background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="%23e3e3e3" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/></svg>') !important;
  116. background-repeat: no-repeat !important;
  117. background-position: right 8px center !important;
  118. background-size: 12px 12px !important;
  119. }
  120. #gemini-snippet-toolbar-userscript option {
  121. background-color: #2a2a2a !important;
  122. color: #e3e3e3 !important;
  123. font-weight: normal !important;
  124. padding: 5px 10px !important;
  125. }
  126. #gemini-snippet-toolbar-userscript button:hover,
  127. #gemini-snippet-toolbar-userscript select:hover {
  128. background-color: #4a4e51 !important;
  129. }
  130. #gemini-snippet-toolbar-userscript button:active {
  131. background-color: #5f6368 !important;
  132. transform: scale(0.98) !important;
  133. }
  134. .userscript-toolbar-spacer {
  135. margin-left: auto !important;
  136. }
  137. `;
  138.  
  139. function injectCustomCSS() {
  140. try {
  141. GM_addStyle(embeddedCSS);
  142. console.log("Gemini Mod Userscript: Custom CSS injected successfully.");
  143. } catch (error) {
  144. console.error("Gemini Mod Userscript: Failed to inject custom CSS:", error);
  145. const styleId = 'gemini-mod-userscript-styles';
  146. if (document.getElementById(styleId)) return;
  147. const style = document.createElement('style');
  148. style.id = styleId;
  149. style.textContent = embeddedCSS;
  150. document.head.appendChild(style);
  151. }
  152. }
  153.  
  154. function displayUserscriptMessage(message, isError = true) {
  155. const prefix = "Gemini Mod Userscript: ";
  156. if (isError) console.error(prefix + message);
  157. else console.log(prefix + message);
  158. alert(prefix + message);
  159. }
  160.  
  161. function moveCursorToEnd(element) {
  162. try {
  163. const range = document.createRange();
  164. const sel = window.getSelection();
  165. range.selectNodeContents(element);
  166. range.collapse(false);
  167. sel.removeAllRanges();
  168. sel.addRange(range);
  169. element.focus();
  170. } catch (e) {
  171. console.error("Gemini Mod Userscript: Error setting cursor position:", e);
  172. }
  173. }
  174.  
  175. function findTargetInputElement() {
  176. let targetInputElement = null;
  177. for (const selector of GEMINI_INPUT_FIELD_SELECTORS) {
  178. const element = document.querySelector(selector);
  179. if (element) {
  180. if (element.classList.contains('ql-editor')) {
  181. const pInEditor = element.querySelector('p');
  182. targetInputElement = pInEditor || element;
  183. } else {
  184. targetInputElement = element;
  185. }
  186. break;
  187. }
  188. }
  189. return targetInputElement;
  190. }
  191.  
  192. function insertSnippetText(textToInsert) {
  193. let targetInputElement = findTargetInputElement();
  194. if (!targetInputElement) {
  195. displayUserscriptMessage("Could not find Gemini input field.");
  196. return;
  197. }
  198. let actualInsertionPoint = targetInputElement;
  199. if (targetInputElement.classList.contains('ql-editor')) {
  200. let p = targetInputElement.querySelector('p');
  201. if (!p) {
  202. p = document.createElement('p');
  203. targetInputElement.appendChild(p);
  204. }
  205. actualInsertionPoint = p;
  206. }
  207. actualInsertionPoint.focus();
  208. setTimeout(() => {
  209. moveCursorToEnd(actualInsertionPoint);
  210. let insertedViaExec = false;
  211. try {
  212. insertedViaExec = document.execCommand('insertText', false, textToInsert);
  213. } catch (e) {
  214. console.warn("Gemini Mod Userscript: execCommand('insertText') threw an error:", e);
  215. }
  216. if (!insertedViaExec) {
  217. if (actualInsertionPoint.innerHTML === '<br>') actualInsertionPoint.innerHTML = '';
  218. actualInsertionPoint.textContent += textToInsert;
  219. moveCursorToEnd(actualInsertionPoint);
  220. }
  221. const editorToDispatchOn = document.querySelector('.ql-editor') || targetInputElement;
  222. if (editorToDispatchOn) {
  223. editorToDispatchOn.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  224. editorToDispatchOn.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
  225. }
  226. console.log("Gemini Mod Userscript: Snippet inserted.");
  227. }, 50);
  228. }
  229.  
  230. async function handlePasteButtonClick() {
  231. try {
  232. if (!navigator.clipboard || !navigator.clipboard.readText) {
  233. displayUserscriptMessage("Clipboard access is not available or not permitted.");
  234. return;
  235. }
  236. const text = await navigator.clipboard.readText();
  237. if (text) insertSnippetText(text);
  238. else console.log("Gemini Mod Userscript: Clipboard is empty.");
  239. } catch (err) {
  240. console.error('Gemini Mod Userscript: Failed to read clipboard contents: ', err);
  241. displayUserscriptMessage(err.name === 'NotAllowedError' ? 'Permission to read clipboard was denied.' : 'Failed to paste from clipboard. See console.');
  242. }
  243. }
  244.  
  245. function ensureLength(filename, maxLength = 255) {
  246. if (filename.length <= maxLength) {
  247. return filename;
  248. }
  249. const dotIndex = filename.lastIndexOf('.');
  250. if (dotIndex === -1 || dotIndex < filename.length - 10 ) {
  251. return filename.substring(0, maxLength);
  252. }
  253. const base = filename.substring(0, dotIndex);
  254. const ext = filename.substring(dotIndex);
  255. const maxBaseLength = maxLength - ext.length;
  256. if (maxBaseLength <= 0) {
  257. return filename.substring(0, maxLength);
  258. }
  259. return base.substring(0, maxBaseLength) + ext;
  260. }
  261.  
  262. function sanitizeBasename(baseName) {
  263. if (typeof baseName !== 'string' || baseName.trim() === "") return "downloaded_document";
  264. let sanitized = baseName.trim()
  265. .replace(INVALID_FILENAME_CHARS_REGEX, '_')
  266. .replace(/\s+/g, '_')
  267. .replace(/__+/g, '_')
  268. .replace(/^[_.-]+|[_.-]+$/g, '');
  269. if (!sanitized || RESERVED_WINDOWS_NAMES_REGEX.test(sanitized)) {
  270. sanitized = `_${sanitized || "file"}_`;
  271. sanitized = sanitized.replace(INVALID_FILENAME_CHARS_REGEX, '_').replace(/\s+/g, '_').replace(/__+/g, '_').replace(/^[_.-]+|[_.-]+$/g, '');
  272. }
  273. return sanitized || "downloaded_document";
  274. }
  275.  
  276. function determineFilename(title, defaultExtension = "txt") {
  277. const logPrefix = "Gemini Mod Userscript: determineFilename - ";
  278. if (!title || typeof title !== 'string' || title.trim() === "") {
  279. console.log(`${logPrefix}Input title invalid or empty, defaulting to "downloaded_document.${defaultExtension}".`);
  280. return ensureLength(`downloaded_document.${defaultExtension}`);
  281. }
  282.  
  283. let trimmedTitle = title.trim();
  284. let baseNamePart = "";
  285. let extensionPart = "";
  286.  
  287. function stripPath(base) {
  288. if (typeof base !== 'string') return base;
  289. const lastSlash = Math.max(base.lastIndexOf('/'), base.lastIndexOf('\\'));
  290. return lastSlash !== -1 ? base.substring(lastSlash + 1) : base;
  291. }
  292.  
  293. const fullTitleMatch = trimmedTitle.match(FILENAME_WITH_EXT_REGEX);
  294. if (fullTitleMatch) {
  295. let potentialBase = fullTitleMatch[1];
  296. const potentialExt = fullTitleMatch[2].toLowerCase();
  297. potentialBase = stripPath(potentialBase);
  298.  
  299. if (!INVALID_FILENAME_CHARS_REGEX.test(potentialBase.replace(/\s/g, '_')) && potentialBase.trim() !== "") {
  300. baseNamePart = potentialBase;
  301. extensionPart = potentialExt;
  302. console.log(`${logPrefix}Entire title "${trimmedTitle}" (path stripped) matches basename.ext. Base: "${baseNamePart}", Ext: "${extensionPart}"`);
  303. }
  304. }
  305.  
  306. if (!extensionPart) {
  307. let lastMatch = null;
  308. let currentMatch;
  309. SUBSTRING_FILENAME_REGEX.lastIndex = 0;
  310. while ((currentMatch = SUBSTRING_FILENAME_REGEX.exec(trimmedTitle)) !== null) {
  311. lastMatch = currentMatch;
  312. }
  313. if (lastMatch) {
  314. const substringCandidate = lastMatch[1];
  315. const substringExtMatch = substringCandidate.match(FILENAME_WITH_EXT_REGEX);
  316. if (substringExtMatch) {
  317. let potentialBaseFromSub = substringExtMatch[1];
  318. const potentialExtFromSub = substringExtMatch[2].toLowerCase();
  319. potentialBaseFromSub = stripPath(potentialBaseFromSub);
  320. if (potentialBaseFromSub.trim() !== "") {
  321. baseNamePart = potentialBaseFromSub;
  322. extensionPart = potentialExtFromSub;
  323. console.log(`${logPrefix}Found substring "${substringCandidate}" (path stripped) matching basename.ext. Base: "${baseNamePart}", Ext: "${extensionPart}"`);
  324. }
  325. }
  326. }
  327. }
  328.  
  329. if (extensionPart) {
  330. const sanitizedBase = sanitizeBasename(baseNamePart);
  331. return ensureLength(`${sanitizedBase}.${extensionPart}`);
  332. } else {
  333. console.log(`${logPrefix}No basename.ext pattern found. Sanitizing full title (path stripped) "${trimmedTitle}" with default extension "${defaultExtension}".`);
  334. const baseForDefault = stripPath(trimmedTitle);
  335. const sanitizedTitleBase = sanitizeBasename(baseForDefault);
  336. return ensureLength(`${sanitizedTitleBase}.${defaultExtension}`);
  337. }
  338. }
  339.  
  340. function triggerDownload(filename, content) {
  341. try {
  342. const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
  343. const url = URL.createObjectURL(blob);
  344. const a = document.createElement('a');
  345. a.href = url;
  346. a.download = filename;
  347. document.body.appendChild(a);
  348. a.click();
  349. document.body.removeChild(a);
  350. URL.revokeObjectURL(url);
  351. console.log(`Gemini Mod Userscript: Download triggered for "${filename}".`);
  352. } catch (error) {
  353. console.error(`Gemini Mod Userscript: Failed to trigger download for "${filename}":`, error);
  354. displayUserscriptMessage(`Failed to download: ${error.message}`);
  355. }
  356. }
  357.  
  358. async function handleGlobalCanvasDownload() {
  359. const titleTextElement = document.querySelector(GEMINI_CANVAS_TITLE_TEXT_SELECTOR);
  360. if (!titleTextElement) {
  361. console.warn("Gemini Mod Userscript: No active canvas title found. Selector:", GEMINI_CANVAS_TITLE_TEXT_SELECTOR);
  362. displayUserscriptMessage("No active canvas found to download.");
  363. return;
  364. }
  365. console.log("Gemini Mod Userscript: Found canvas title element:", titleTextElement);
  366.  
  367. const codeImmersivePanelElement = titleTextElement.closest('code-immersive-panel');
  368. if (!codeImmersivePanelElement) {
  369. console.warn("Gemini Mod Userscript: Could not find parent 'code-immersive-panel' for the title element.");
  370. displayUserscriptMessage("Could not locate the main canvas panel for the active canvas.");
  371. return;
  372. }
  373. console.log("Gemini Mod Userscript: Found 'code-immersive-panel' element:", codeImmersivePanelElement);
  374. const shareButton = codeImmersivePanelElement.querySelector(GEMINI_CANVAS_SHARE_BUTTON_SELECTOR);
  375. if (!shareButton) {
  376. console.warn("Gemini Mod Userscript: 'Share' button not found within 'code-immersive-panel'. Selector used:", GEMINI_CANVAS_SHARE_BUTTON_SELECTOR);
  377. displayUserscriptMessage("Could not find the 'Share' button in the active canvas's panel.");
  378. return;
  379. }
  380. console.log("Gemini Mod Userscript: Found 'Share' button:", shareButton);
  381. shareButton.click();
  382. console.log("Gemini Mod Userscript: Programmatically clicked the 'Share' button.");
  383.  
  384. // Wait for the copy button (potentially in a modal/overlay) to appear
  385. setTimeout(() => {
  386. const copyButton = document.querySelector(GEMINI_CANVAS_COPY_BUTTON_SELECTOR);
  387. if (!copyButton) {
  388. console.warn("Gemini Mod Userscript: 'Copy to Clipboard' button not found globally after clicking share. Selector used:", GEMINI_CANVAS_COPY_BUTTON_SELECTOR);
  389. displayUserscriptMessage("Could not find the 'Copy to Clipboard' button after clicking share.");
  390. return;
  391. }
  392. console.log("Gemini Mod Userscript: Found 'Copy to Clipboard' button globally:", copyButton);
  393.  
  394. copyButton.click();
  395. console.log("Gemini Mod Userscript: Programmatically clicked the 'Copy to Clipboard' button.");
  396.  
  397. setTimeout(async () => {
  398. try {
  399. if (!navigator.clipboard || !navigator.clipboard.readText) {
  400. displayUserscriptMessage("Clipboard access not available.");
  401. return;
  402. }
  403. const clipboardContent = await navigator.clipboard.readText();
  404. console.log("Gemini Mod Userscript: Successfully read from clipboard.");
  405. if (!clipboardContent || clipboardContent.trim() === "") {
  406. displayUserscriptMessage("Clipboard empty after copy. Nothing to download.");
  407. return;
  408. }
  409. const canvasTitle = (titleTextElement.textContent || "Untitled Canvas").trim();
  410. const filename = determineFilename(canvasTitle);
  411. triggerDownload(filename, clipboardContent);
  412. console.log("Gemini Mod Userscript: Global download initiated for canvas title:", canvasTitle, "using clipboard content. Filename:", filename);
  413. } catch (err) {
  414. console.error('Gemini Mod Userscript: Error reading from clipboard:', err);
  415. displayUserscriptMessage(err.name === 'NotAllowedError' ? 'Clipboard permission denied.' : 'Failed to read clipboard.');
  416. }
  417. }, 300); // Delay for clipboard write
  418. }, 500); // Delay for share menu to open and copy button to appear
  419. }
  420.  
  421. function createToolbar() {
  422. const toolbarId = 'gemini-snippet-toolbar-userscript';
  423. if (document.getElementById(toolbarId)) {
  424. console.log("Gemini Mod Userscript: Toolbar already exists.");
  425. return;
  426. }
  427. console.log("Gemini Mod Userscript: Initializing toolbar...");
  428. const toolbar = document.createElement('div');
  429. toolbar.id = toolbarId;
  430. buttonSnippets.forEach(snippet => {
  431. const button = document.createElement('button');
  432. button.textContent = snippet.label;
  433. button.title = snippet.text;
  434. button.addEventListener('click', () => insertSnippetText(snippet.text));
  435. toolbar.appendChild(button);
  436. });
  437. dropdownConfigurations.forEach(config => {
  438. if (config.options && config.options.length > 0) {
  439. const select = document.createElement('select');
  440. select.title = config.placeholder || "Select snippet";
  441. const defaultOption = document.createElement('option');
  442. defaultOption.textContent = config.placeholder || "Select...";
  443. defaultOption.value = "";
  444. defaultOption.disabled = true;
  445. defaultOption.selected = true;
  446. select.appendChild(defaultOption);
  447. config.options.forEach(snippet => {
  448. const option = document.createElement('option');
  449. option.textContent = snippet.label;
  450. option.value = snippet.text;
  451. select.appendChild(option);
  452. });
  453. select.addEventListener('change', (event) => {
  454. const selectedText = event.target.value;
  455. if (selectedText) {
  456. insertSnippetText(selectedText);
  457. event.target.selectedIndex = 0;
  458. }
  459. });
  460. toolbar.appendChild(select);
  461. }
  462. });
  463. const spacer = document.createElement('div');
  464. spacer.className = 'userscript-toolbar-spacer';
  465. toolbar.appendChild(spacer);
  466. const pasteButton = document.createElement('button');
  467. pasteButton.textContent = PASTE_BUTTON_LABEL;
  468. pasteButton.title = "Paste from Clipboard";
  469. pasteButton.addEventListener('click', handlePasteButtonClick);
  470. toolbar.appendChild(pasteButton);
  471. const globalDownloadButton = document.createElement('button');
  472. globalDownloadButton.textContent = DOWNLOAD_BUTTON_LABEL;
  473. globalDownloadButton.title = "Download active canvas content (uses canvas's copy button)";
  474. globalDownloadButton.addEventListener('click', handleGlobalCanvasDownload);
  475. toolbar.appendChild(globalDownloadButton);
  476. document.body.insertBefore(toolbar, document.body.firstChild);
  477. console.log("Gemini Mod Userscript: Toolbar inserted.");
  478. }
  479.  
  480. function handleDarkModeForUserscript() {
  481. console.log("Gemini Mod Userscript: Dark mode handling is passive (toolbar is dark by default).");
  482. }
  483.  
  484. // --- Initialization Logic ---
  485. function init() {
  486. console.log("Gemini Mod Userscript: Initializing...");
  487. injectCustomCSS();
  488. const M_INITIALIZATION_DELAY = 1500;
  489. setTimeout(() => {
  490. try {
  491. createToolbar();
  492. handleDarkModeForUserscript();
  493. console.log("Gemini Mod Userscript: Fully initialized.");
  494. } catch(e) {
  495. console.error("Gemini Mod Userscript: Error during delayed initialization:", e);
  496. displayUserscriptMessage("Error initializing toolbar. See console.");
  497. }
  498. }, M_INITIALIZATION_DELAY);
  499. }
  500.  
  501. if (document.readyState === 'loading') {
  502. window.addEventListener('DOMContentLoaded', init);
  503. } else {
  504. init();
  505. }
  506.  
  507. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址