让 Bitwarden / 1Password / LastPass 等密码管理器更容易识别各网站的 2FA 验证码/二次校验码输入框(含弹窗、SPA)
// ==UserScript==
// @name 通用 2FA / 验证码 自动填充辅助(密码管理器优化)
// @namespace https://www.02id.com/
// @version 2025.11.29
// @description 让 Bitwarden / 1Password / LastPass 等密码管理器更容易识别各网站的 2FA 验证码/二次校验码输入框(含弹窗、SPA)
// @author 萌新 & 零贰博客
// @icon https://www.02id.com/favicon.png
// @match *://*/*
// @run-at document-end
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
DEBUG: false,
POLL_INTERVAL: 800,
MAX_POLL_ATTEMPTS: 120,
TRIGGER_EVENTS: true,
FORCE_NUMERIC_INPUTMODE: true,
// 会被排除的图形验证码特征
CAPTCHA_KEYWORDS: [
"图形验证码", "图片验证码", "请输验证码", "请输入验证码",
"看不清", "点击刷新", "刷新验证码"
],
CAPTCHA_NAME_KEYS: ["captcha", "verify", "checkcode", "imgcode"],
// 2FA 关键词
OTP_KEYWORDS: [
'验证码', '校验码', '二次校验码', '动态码', '动态密码', '安全码',
'otp', 'one time code', 'one-time code', 'verification code',
'auth code', '2fa', 'two factor', 'mfa'
],
SITE_RULES: [
{
name: '阿里云 2FA',
urlPattern: /passport\.alibabacloud\.com\/ac\/iv\/mini\/identity_verify\.htm/i,
selectors: ['#J_Tp_Checkcode']
},
{
name: 'ElementUI 弹窗 二次校验码(常见后台)',
urlPattern: /.*/,
selectors: [
'div.el-dialog input.el-input__inner[placeholder*="二次校验"]',
'div.el-dialog input.el-input__inner[placeholder*="验证码"]'
]
}
],
COMMON_LENGTHS: [4,5,6,7,8]
};
function debug(msg) {
if (CONFIG.DEBUG) console.log('[2FA Helper]', msg);
}
// 🛡️ 图形验证码排除判断
function isCaptchaInput(el) {
const placeholder = (el.placeholder || "").toLowerCase();
const name = (el.name || "").toLowerCase();
const id = (el.id || "").toLowerCase();
// placeholder 包含图形验证码关键词
for (const kw of CONFIG.CAPTCHA_KEYWORDS) {
if (placeholder.includes(kw.toLowerCase())) return true;
}
// 名称包含 captcha 常用字段
for (const key of CONFIG.CAPTCHA_NAME_KEYS) {
if (name.includes(key) || id.includes(key)) return true;
}
// 若有 <img> 紧挨着(常见验证码布局)
const parent = el.closest(".form-group, .input-group, .el-input, div");
if (parent && parent.querySelector("img")) {
const img = parent.querySelector("img");
if (img.src && img.src.match(/captcha|verify|image|code/i)) {
return true;
}
}
return false;
}
function isLikelyOtpInput(el) {
if (!el || el.tagName !== 'INPUT') return false;
// ⛔ 排除图形验证码
if (isCaptchaInput(el)) {
debug("排除图形验证码输入框");
return false;
}
const type = (el.type || '').toLowerCase();
if (!['text', 'tel', 'number', 'password'].includes(type)) return false;
const placeholder = (el.placeholder || '').toLowerCase();
const name = (el.name || '').toLowerCase();
const id = (el.id || '').toLowerCase();
const aria = (el.getAttribute('aria-label') || '').toLowerCase();
const maxLength = parseInt(el.maxLength || "0", 10);
const combined = [placeholder, name, id, aria].join(" ");
// OTP 关键词检测
for (const kw of CONFIG.OTP_KEYWORDS)
if (combined.includes(kw.toLowerCase())) return true;
// 数字长度推断 (非密码框)
if (!isNaN(maxLength) && CONFIG.COMMON_LENGTHS.includes(maxLength)) {
const pwLike = /password|passwd|pwd/.test(combined);
if (!pwLike) return true;
}
return false;
}
function enhanceOtpInput(el, reason) {
if (el.dataset._enhanced === "1") return;
el.dataset._enhanced = "1";
debug("增强 2FA 输入框: " + reason);
el.setAttribute("autocomplete", "one-time-code");
el.setAttribute("data-lpignore", "false");
el.setAttribute("data-1p-ignore", "false");
if (CONFIG.FORCE_NUMERIC_INPUTMODE) {
el.setAttribute("inputmode", "numeric");
el.setAttribute("pattern", "\\d*");
}
if (CONFIG.TRIGGER_EVENTS) {
el.dispatchEvent(new Event("focus", { bubbles: true }));
el.dispatchEvent(new Event("input", { bubbles: true }));
}
}
function scan(root) {
const inputs = root.querySelectorAll("input");
inputs.forEach(el => {
if (isLikelyOtpInput(el)) enhanceOtpInput(el, "heuristic");
});
}
function init() {
scan(document);
const obs = new MutationObserver(list => {
list.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType === 1) scan(node);
});
});
});
obs.observe(document.body, { childList: true, subtree: true });
let attempts = 0;
const timer = setInterval(() => {
attempts++;
scan(document);
if (attempts >= CONFIG.MAX_POLL_ATTEMPTS) clearInterval(timer);
}, CONFIG.POLL_INTERVAL);
}
document.readyState === "loading"
? document.addEventListener("DOMContentLoaded", init)
: init();
})();