您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 AutoDL 控制台实例列表显示 JupyterLab URL 复制按钮 (含 token)
// ==UserScript== // @name AutoDL JupyterLab URL Copier // @namespace https://github.com/SomeBottle/fastfood // @version 2025-08-13-02 // @description 在 AutoDL 控制台实例列表显示 JupyterLab URL 复制按钮 (含 token) // @author SomeBottle // @match https://www.autodl.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=autodl.com // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; // 本脚本的前缀 const SCRIPT_PREFIX = '[JupyterLab URL Copier]'; // AutoDL 的请求基于 XMLHttpRequest, 拦截改写 const INSTANCE_API_URL_PATTERN = /\/api\/.*?\/instance$/i; // 复制按钮的样式 const COPY_BTN_STYLE_SHEET = ` .jupyter-url-copy-btn{ position: relative; display: block; color: #46a0ffff; text-decoration: none; transition: .5s ease; } .jupyter-url-copy-btn:hover{ color: #1cced7ff; } .jupyter-url-copy-btn::after{ position: absolute; z-index: 900; content: "(๑‾ ꇴ ‾๑) 已复制"; left: 0; top: 50%; width: 100%; text-align: left; pointer-events: none; transform: translateY(-50%); transition: .5s ease; opacity: 0; background-color: #ffffffbf; } .copied-notice::after{ opacity: 1; } `; // 存储实例 UUID 到 JupyterLab URL 的映射 const instanceId2Url = {}; // 存储实例 UUID 到 DOM 节点的映射 const instanceId2Node = {}; // 存储上一次拦截结果中出现的 URL let prevInstanceUrls = []; // 哨兵 DOM 元素,用于检测页面中是否有按钮存在 let sentryNode = null; // 比较数组是否完全一致 const arraysEqual = function (a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; }; // 找到页面中 instanceId 出现的元素 const searchNodes = function (node) { if (node.nodeType === Node.ELEMENT_NODE) { let nodeText = node.innerText; if (nodeText) { nodeText = nodeText.trim(); if (nodeText in instanceId2Url) { instanceId2Node[nodeText] = node; } } } node.childNodes.forEach(child => searchNodes(child)); }; // 拦截 XMLHttpRequest 请求响应 const requestHook = function () { const response = JSON.parse(this.responseText); if (response.data) { const instanceList = response.data.list; let currentInstanceUrls = []; for (let instance of instanceList) { let url = `https://${instance.jupyter_domain}/jupyter/?token=${instance.jupyter_token}`; if (instance.jupyter_port !== 0) { // 根据 AutoDL 的接口前端处理逻辑,如果 jupyter_port 不为 0,则采用 proxy_host + port,且为 http 协议 url = `http://${instance.proxy_host}:${instance.jupyter_port}/jupyter/?token=${instance.jupyter_token}`; } currentInstanceUrls.push(url); instanceId2Url[instance.uuid] = url; } if (sentryNode !== null && document.contains(sentryNode) && arraysEqual(currentInstanceUrls, prevInstanceUrls)) { // 如果当前实例 URL 与上一次相同,则不需要更新,减少资源浪费 console.log(`${SCRIPT_PREFIX} No changes in instance URLs.`); return; } prevInstanceUrls = currentInstanceUrls; // 搜索 DOM 中相应的结点 searchNodes(document.body); for (let insId in instanceId2Url) { let insNode = instanceId2Node[insId]; let copyBtnId = `jupyter-url-copy-btn-${insId}`; if (!insNode) { // 如果节点不存在,则跳过 continue; } if (!document.contains(insNode)) { // 如果节点不在文档中,则跳过,并移除掉映射 // 这种情况发生在用户释放了实例,页面中项目消失,但是 instanceId2Node 还存着 delete instanceId2Node[insId]; continue; } // 防止元素重复添加 let btnElement = document.getElementById(copyBtnId); if (btnElement === null) { // 在这个结点的后面添加一个复制 URL 的按钮 let copyBtn = document.createElement('a'); copyBtn.innerText = '复制 JupyterLab URL'; copyBtn.href = 'javascript:void(0)'; copyBtn.classList.add('jupyter-url-copy-btn'); copyBtn.id = copyBtnId; sentryNode = copyBtn; insNode.insertAdjacentElement('afterend', copyBtn); console.log(`${SCRIPT_PREFIX} Added copy button of the instance: ${insId}`); btnElement = copyBtn; // 因为按钮的生命周期不受 AutoDL 前端管理(比如删除实例项后按钮还在),需要手动管理 // 在对应项目消失时,按钮一并消失 (function (btn, insNode, insId) { let observer = new MutationObserver(() => { // 元素可能被移除,也可能是内容被替换 if (!document.contains(insNode) || insNode.innerText.trim() !== insId) { btn.remove(); observer.disconnect(); } }); observer.observe(insNode.parentNode, { childList: true }); })(btnElement, insNode, insId); } // 因为 URL 变动了,就算元素存在,也要更新 URL btnElement.onclick = function () { navigator.clipboard.writeText(instanceId2Url[insId]); // 复制后的提示 this.classList.add('copied-notice'); setTimeout(() => { this.classList.remove('copied-notice'); }, 1200); }; } } this.removeEventListener('load', requestHook); }; const originalOpen = XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (method, url, ...args) { if (INSTANCE_API_URL_PATTERN.test(url)) { // 如果是指定 URL 则开始监听 this.addEventListener('load', requestHook.bind(this)); } return originalOpen.apply(this, [method, url, ...args]); }; // 把按钮样式加入 DOM const btnStyle = document.createElement('style'); btnStyle.innerHTML = COPY_BTN_STYLE_SHEET; document.head.appendChild(btnStyle); console.log(`${SCRIPT_PREFIX} JupyterLab URL Copier Loaded, have fun~ /ᐠ。ꞈ。ᐟ\\`); })();