佬友分享 AI Key 时,会进行 Base64 编码, 用Key,都得复制→各种操作解码→再复制,切来切去。周末动手搓了个油猴脚本。
完全寄生在论坛自带的交互上:
- 随便选一段 Base64 密文
- 论坛会自动弹出那个带“引用”“分享”的小浮动菜单
- 菜单里多了一个 “^-^ Base64解码” 按钮,点它
- 解码结果会直接以代码块的形式插入到选区下方,还带 复制 和 关闭 按钮
5Luj56CB5Y2D5LiH6KGM77yM5pG46bG856ys5LiA5p2h77yb5YiH5bGP5LiN6KeE6IyD77yM6ICB5p2/5Lik6KGM55uu44CC

脚本代码
// ==UserScript==
// @name Linux.do Base64解码
// @namespace https://linux.do/
// @version 3.0
// @description 选中Base64文本后,在弹出菜单点击解码,原文下方插入可复制的代码块
// @author hum&&deepseek
// @match https://linux.do/t/topic/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// 解码 Base64,支持 UTF-8 多字节字符
function decodeBase64ToUnicode(str) {
try {
const binary = atob(str.trim());
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder('utf-8').decode(bytes);
} catch {
return null;
}
}
// 在选区末尾插入带复制和关闭按钮的代码块
function insertDecodedBlock(decodedText, range) {
// 外层容器,模拟论坛代码块样式
const wrapper = document.createElement('div');
wrapper.className = 'md-codeblock';
Object.assign(wrapper.style, {
position: 'relative',
marginTop: '8px',
marginBottom: '8px'
});
const pre = document.createElement('pre');
const code = document.createElement('code');
code.textContent = decodedText;
pre.appendChild(code);
wrapper.appendChild(pre);
// 按钮工具栏
const toolbar = document.createElement('div');
toolbar.className = 'codeblock-button-wrapper';
Object.assign(toolbar.style, {
position: 'absolute',
top: '4px',
right: '4px',
display: 'flex',
gap: '4px'
});
// --- 复制按钮(完全自己控制)---
const copyBtn = document.createElement('button');
copyBtn.className = 'btn nohighlight btn-flat';
copyBtn.setAttribute('aria-label', '复制解码文本');
copyBtn.innerHTML = '<svg class="fa d-icon d-icon-copy svg-icon fa-width-auto svg-string" width="1em" height="1em" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#copy"></use></svg>';
// 点击复制,并显示“已复制!”
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(decodedText).then(() => {
copyBtn.innerHTML = '已复制!';
copyBtn.style.pointerEvents = 'none';
setTimeout(() => {
copyBtn.innerHTML = '<svg class="fa d-icon d-icon-copy svg-icon fa-width-auto svg-string" width="1em" height="1em" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#copy"></use></svg>';
copyBtn.style.pointerEvents = '';
}, 2000);
}).catch(() => alert('复制失败,请手动复制'));
});
// --- 关闭按钮 ---
const closeBtn = document.createElement('button');
closeBtn.className = 'btn nohighlight btn-flat';
closeBtn.innerHTML = '✖';
closeBtn.title = '关闭';
closeBtn.addEventListener('click', () => wrapper.remove());
toolbar.appendChild(copyBtn);
toolbar.appendChild(closeBtn);
wrapper.appendChild(toolbar);
// 插入到选区末尾
range.collapse(false);
range.insertNode(wrapper);
range.setStartAfter(wrapper);
range.collapse(true);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// 在浮动菜单中添加解码按钮
function addDecodeButton(menu) {
if (menu.dataset.base64DecodeAdded) return;
menu.dataset.base64DecodeAdded = 'true';
// 优先查找 .quote-button 内的 .buttons,兼容多种结构
const btns = menu.querySelector('.quote-button .buttons') || menu.querySelector('.buttons');
if (!btns) return;
const decodeBtn = document.createElement('button');
decodeBtn.className = 'btn btn-icon-text btn-flat';
decodeBtn.title = 'Base64 解码';
decodeBtn.type = 'button';
decodeBtn.innerHTML = '<span class="fa d-icon">🔓</span><span class="d-button-label">Base64解码</span>';
decodeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const selection = window.getSelection();
if (!selection.rangeCount || !selection.toString().trim()) {
alert('请先选中一段 Base64 文本');
return;
}
const raw = selection.toString().trim();
const decoded = decodeBase64ToUnicode(raw);
if (decoded === null) {
alert('解码失败,请确认内容是有效的 Base64 编码');
return;
}
insertDecodedBlock(decoded, selection.getRangeAt(0));
// 点击一下 body 关闭浮动菜单
document.body.click();
});
btns.appendChild(decodeBtn);
}
// 监听 #d-menu-portals 内菜单的创建
function startObserver() {
const portal = document.getElementById('d-menu-portals');
if (!portal) {
setTimeout(startObserver, 500);
return;
}
// 处理已存在的菜单
portal.querySelectorAll('div.fk-d-menu').forEach(addDecodeButton);
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
for (const node of mut.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches && node.matches('div.fk-d-menu')) {
addDecodeButton(node);
}
if (node.querySelectorAll) {
node.querySelectorAll('div.fk-d-menu').forEach(addDecodeButton);
}
}
}
}
});
observer.observe(portal, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', startObserver);
} else {
startObserver();
}
})();