Skip to content

Instantly share code, notes, and snippets.

@zhuziyi1989
Last active October 21, 2025 17:27
Show Gist options
  • Save zhuziyi1989/c728adf9c1d569e74362ca00818fe979 to your computer and use it in GitHub Desktop.
Save zhuziyi1989/c728adf9c1d569e74362ca00818fe979 to your computer and use it in GitHub Desktop.
油猴插件:在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果

评论区回复测试


@wangdada #138 怎么方便的解密 Base64 啊?有没有插件推荐一个。

试试我分享的油猴脚本在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果,测试效果如下。

① 、未使用插件时显示

1

[未使用插件时显示](https://i.imgur.com/Joi0n9p.png)

② 、使用插件后显示为

2

[使用插件后显示为](https://i.imgur.com/J5sqJNo.png)

③ 、在评论区选中输入的文本可直接解密、加密替换

4-min

[在评论区选中输入的文本可直接解密、加密替换-GIF演示](https://i.imgur.com/qwgmHdN.gif)

以下为测试数据:

国内打开 aHR0cHM6Ly8xcy5iaWdtZW9rLm1lL3VzZXIjL3JlZ2lzdGVyP2NvZGU9SW92V09wS0Y=

科学上网 aHR0cHM6Ly93d3cuYmlnbWUucHJvL3VzZXIjL3JlZ2lzdGVyP2NvZGU9a2JYRjNrSmU=

我是一个测试文本 5oiR5piv5LiA5Liq5rWL6K+V5paH5pys

我是一个测试邮箱 cm9vdEBnbWFpbC5jb20=

// ==UserScript==
// @name V2EX Base64 Enhance 自动解析增强版
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果;若解码为 URL 则渲染为可点击链接(在新标签打开)。仅处理帖子正文与回复内容,避免页面卡死。
// @author zhuziyi
// @match https://v2ex.com/t/*
// @match https://*.v2ex.com/t/*
// @grant none
// @run-at document-idle
// @supportURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
// @homepageURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
// @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/97adee1329d4de9f6ef21eeb500006dea9998508/V2EX%2520Base64%2520Enhance.js
// @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/97adee1329d4de9f6ef21eeb500006dea9998508/V2EX%2520Base64%2520Enhance.js
// ==/UserScript==
(function () {
"use strict";
const MIN_B64_LEN = 16; // 最小 Base64 长度阈值,按需调整
const SKIP_TAGS = new Set([
"SCRIPT",
"STYLE",
"CODE",
"PRE",
"TEXTAREA",
"INPUT",
"A",
]);
const B64_RE = new RegExp(
`([A-Za-z0-9_\\-+\\/]{${MIN_B64_LEN},}={0,2})`,
"g"
);
const APP_MARK = "data-b64-script"; // 我们插入节点时会打上这个标记
// 记录已处理过的文本节点,避免重复处理
const processedTextNodes = new WeakSet();
function normalizeBase64(b64) {
let s = b64.replace(/\s+/g, "");
s = s.replace(/-/g, "+").replace(/_/g, "/");
const pad = s.length % 4;
if (pad === 1) return null;
if (pad !== 0) s += "=".repeat(4 - pad);
return s;
}
function decodeBase64ToString(b64) {
try {
const norm = normalizeBase64(b64);
if (!norm) return null;
const binary = atob(norm);
// binary 是 Latin1,需要转为 UTF-8
let percentEncoded = "";
for (let i = 0; i < binary.length; i++) {
const code = binary.charCodeAt(i);
percentEncoded += "%" + ("00" + code.toString(16)).slice(-2);
}
try {
return decodeURIComponent(percentEncoded);
} catch (e) {
// 退回为原 binary(可能是 Latin1 文本)
return binary;
}
} catch (e) {
return null;
}
}
function looksReadable(s) {
if (!s || s.length < 3) return false;
if (/[\u4e00-\u9fff]/.test(s)) return true; // 含中文
if (/(https?:\/\/|www\.)/i.test(s)) return true;
// 至少有 60% 的可打印 ASCII 字符
const printableCount = (s.match(/[\x20-\x7E]/g) || []).length;
if (printableCount / s.length >= 0.6) return true;
return false;
}
function createAppendNode(decodedText) {
const wrapper = document.createElement("span");
wrapper.setAttribute(APP_MARK, "1");
wrapper.style.marginLeft = "6px";
wrapper.style.fontSize = "0.95em";
wrapper.style.verticalAlign = "baseline";
const trimmed = decodedText.trim();
const isURL = /^https?:\/\//i.test(trimmed);
if (isURL) {
// 全角括号包裹的可点链接
const left = document.createTextNode("(");
const right = document.createTextNode(")");
const a = document.createElement("a");
a.href = trimmed;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.textContent = trimmed;
// 不强制设置颜色,保持与站点主题兼容;若需自定义可修改以下两行
a.style.textDecoration = "underline";
a.style.cursor = "pointer";
wrapper.appendChild(left);
wrapper.appendChild(a);
wrapper.appendChild(right);
} else {
wrapper.style.color = "#888";
wrapper.textContent = `(${trimmed})`;
}
return wrapper;
}
function processTextNode(textNode) {
if (!textNode || !textNode.nodeValue) return;
if (processedTextNodes.has(textNode)) return;
const parent = textNode.parentNode;
if (!parent || SKIP_TAGS.has(parent.tagName)) return;
// 如果父元素或祖先已是我们插入内容的标记,则跳过(避免在我们的 span 内再次处理)
if (parent.closest && parent.closest("[" + APP_MARK + "]")) return;
const text = textNode.nodeValue;
B64_RE.lastIndex = 0;
let match;
let lastIndex = 0;
const frag = document.createDocumentFragment();
let anyMatch = false;
while ((match = B64_RE.exec(text)) !== null) {
const b64 = match[1];
// 把前面的纯文本加入
const before = text.slice(lastIndex, match.index);
if (before) frag.appendChild(document.createTextNode(before));
// 原 Base64 文本(保持原样)
const origNode = document.createTextNode(b64);
frag.appendChild(origNode);
// 尝试解码并判断是否可读
const decoded = decodeBase64ToString(b64);
if (decoded && looksReadable(decoded)) {
const appendNode = createAppendNode(decoded);
frag.appendChild(appendNode);
}
lastIndex = match.index + b64.length;
anyMatch = true;
}
if (!anyMatch) {
// 标记为已检查过(避免下次又检查同一节点)
processedTextNodes.add(textNode);
return;
}
// 尾部文本
const tail = text.slice(lastIndex);
if (tail) frag.appendChild(document.createTextNode(tail));
// 替换原 text node(注意:替换后原节点不再存在)
try {
parent.replaceChild(frag, textNode);
} catch (e) {
// 如果替换失败(极少见),仍将该节点标记以避免重复
processedTextNodes.add(textNode);
return;
}
// 无需标记新创建的文本节点;但为了避免再次检查同一位置,我们可以标记父元素(轻量)
// 在父元素上打一个短期属性,表明它已经被扫描(不会影响页面)
try {
parent.setAttribute("data-b64-scanned", Date.now().toString());
} catch (e) {
// 忽略不可设置属性的情况(例如某些 SVG 节点)
}
}
function walkAndProcess(root) {
if (!root) return;
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
if (!node.nodeValue || !node.nodeValue.trim())
return NodeFilter.FILTER_REJECT;
const p = node.parentNode;
if (!p) return NodeFilter.FILTER_REJECT;
if (SKIP_TAGS.has(p.tagName)) return NodeFilter.FILTER_REJECT;
if (p.closest && p.closest("[" + APP_MARK + "]"))
return NodeFilter.FILTER_REJECT;
if (processedTextNodes.has(node)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
},
false
);
const nodes = [];
let n;
while ((n = walker.nextNode())) nodes.push(n);
for (const tn of nodes) processTextNode(tn);
}
// 找到需要处理的区域(帖子正文与回复)
function findTargets() {
const nodes = [];
const main = document.querySelector(".topic_content");
if (main) nodes.push(main);
// 回复区常见类名
document
.querySelectorAll(".reply_content, .message, .topic .cell .reply_content")
.forEach((n) => {
if (n) nodes.push(n);
});
return nodes;
}
function initScan() {
const targets = findTargets();
if (targets.length === 0) return;
for (const t of targets) walkAndProcess(t);
}
// 监听帖子区域的变化(只对目标区域设置 observer)
const observers = [];
function initObservers() {
const targets = findTargets();
if (targets.length === 0) return;
for (const t of targets) {
const mo = new MutationObserver((mutations) => {
for (const m of mutations) {
// 跳过我们自己插入的节点(根节点带标记)
if (m.addedNodes && m.addedNodes.length) {
for (const node of m.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
// 如果新增节点是我们自己创建的标记子树,跳过
if (node.hasAttribute && node.hasAttribute(APP_MARK)) continue;
// 如果新增元素内部含有我们的标记,也跳过
if (
node.querySelector &&
node.querySelector("[" + APP_MARK + "]")
)
continue;
// 对新增元素内部做扫描
walkAndProcess(node);
} else if (node.nodeType === Node.TEXT_NODE) {
// 直接处理文本节点
processTextNode(node);
}
}
}
if (m.type === "characterData" && m.target) {
processTextNode(m.target);
}
}
});
mo.observe(t, { childList: true, subtree: true, characterData: true });
observers.push(mo);
}
}
// 初次运行
initScan();
initObservers();
// 暴露一个手动触发的函数(控制台可用),方便调试/手动重新扫描
window.__v2ex_b64_rescan = function () {
initScan();
console.log("v2ex-base64: manual rescan done");
};
console.log("v2ex-base64 (post-only) loaded");
// ============ 新增:Base64 加密与解密 选中文本 ============
// Base64 编码
function encodeBase64(str) {
try {
return btoa(unescape(encodeURIComponent(str)));
} catch (e) {
return str;
}
}
// Base64 解码
function decodeBase64(str) {
try {
return decodeURIComponent(escape(atob(str)));
} catch (e) {
return str;
}
}
// ============ 新:自动隐藏按钮 ============
function updateButtonVisibility() {
const textarea = document.querySelector("#reply_content");
const encodeBtn = document.getElementById("base64_encode_btn");
const decodeBtn = document.getElementById("base64_decode_btn");
if (!textarea || !encodeBtn || !decodeBtn) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// 无选区 → 按钮隐藏,但保留占位
if (start === end) {
encodeBtn.style.visibility = "hidden";
decodeBtn.style.visibility = "hidden";
} else {
encodeBtn.style.visibility = "visible";
decodeBtn.style.visibility = "visible";
}
}
function addBase64Buttons() {
const replyBtn = document.querySelector(
'input[type="submit"][value="回复"]'
);
replyBtn.parentElement.style.justifyContent = "flex-start";
replyBtn.parentElement.style.flexDirection = "row-reverse";
const textarea = document.querySelector("#reply_content");
if (!replyBtn || !textarea) return;
if (document.getElementById("base64_encode_btn")) return;
// 按钮创建器
function createBtn(text, id) {
const btn = document.createElement("button");
btn.id = id;
btn.textContent = text;
btn.style.marginRight = "8px";
btn.style.padding = "5px 8px";
btn.style.border = "1px solid #f5f5f5";
btn.style.background = "#f5f5f5";
btn.style.borderRadius = "5px";
btn.style.cursor = "pointer";
btn.style.fontSize = "12px";
btn.style.lineHeight = "1.4";
btn.style.color = "#9349ff";
btn.style.visibility = "hidden"; // 初始隐藏
return btn;
}
const encodeBtn = createBtn("🔐 Base64编码", "base64_encode_btn");
encodeBtn.addEventListener("click", (e) => {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const txt = textarea.value.substring(start, end);
textarea.value =
textarea.value.substring(0, start) +
encodeBase64(txt) +
textarea.value.substring(end);
textarea.selectionStart = textarea.selectionEnd =
start + encodeBase64(txt).length;
textarea.focus();
updateButtonVisibility();
});
const decodeBtn = createBtn("🔓 Base64解码", "base64_decode_btn");
decodeBtn.addEventListener("click", (e) => {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const txt = textarea.value.substring(start, end);
textarea.value =
textarea.value.substring(0, start) +
decodeBase64(txt) +
textarea.value.substring(end);
textarea.selectionStart = textarea.selectionEnd =
start + decodeBase64(txt).length;
textarea.focus();
updateButtonVisibility();
});
replyBtn.parentNode.insertBefore(encodeBtn, replyBtn.nextSibling);
replyBtn.parentNode.insertBefore(decodeBtn, encodeBtn.nextSibling);
// 监听 selection 改变 → 控制可见性
["select", "keyup", "mouseup"].forEach((evt) => {
textarea.addEventListener(evt, updateButtonVisibility);
});
}
setTimeout(addBase64Buttons, 1500);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment