Files
auto-login/content.js
T
2026-09-16 14:41:05 +08:00

558 lines
18 KiB
JavaScript

const t = (key, ...args) => chrome.i18n.getMessage(key, args) || key;
async function safeStorageGet(area, keys) {
try {
return await chrome.storage[area].get(keys);
} catch {
return {};
}
}
async function safeStorageSet(area, items) {
try {
await chrome.storage[area].set(items);
} catch {}
}
// 2FA / 验证码检测:检测到则不自动提交,仅填充
function detectCaptcha() {
// reCAPTCHA
if (document.querySelector(".g-recaptcha, [data-sitekey], iframe[src*='recaptcha'], iframe[src*='google.com/recaptcha']")) {
return { type: "reCAPTCHA", reason: t("captchaTitleRe") };
}
// hCaptcha
if (document.querySelector(".h-captcha, iframe[src*='hcaptcha']")) {
return { type: "hCaptcha", reason: t("captchaTitleH") };
}
// 国内常见滑块/验证码容器
const sliderSelectors = [
"#slider", ".slider", ".nc_wrapper", ".nc_iconfont",
"#captcha", ".captcha", ".geetest", "#geetest",
"[class*='slider']", "[id*='slider']", "[id*='captcha']", "[class*='captcha']",
"canvas[id*='captcha']", "canvas[id*='verify']"
];
for (const sel of sliderSelectors) {
try {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
return { type: "slider/captcha", reason: t("captchaReasonSlider", sel) };
}
} catch { /* selector 可能无效 */ }
}
// 明显的"验证码"文字输入框(带图形码)
const codeInputs = document.querySelectorAll(
"input[name*='captcha' i], input[name*='verify' i], input[name*='code' i], input[id*='captcha' i], input[id*='verify' i], input[placeholder*='验证码' i], input[placeholder*='验证' i]"
);
for (const input of codeInputs) {
if (input.offsetParent !== null && input.type !== "hidden") {
return { type: "captcha-input", reason: t("captchaReasonInput") };
}
}
return null;
}
function setNativeValue(el, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set || Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
)?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
function findUsernameField(config) {
if (config.usernameSelector) {
const el = document.querySelector(config.usernameSelector);
if (el) return el;
}
const passwordField = findPasswordField(config);
if (!passwordField) return null;
const form = passwordField.closest("form");
const searchScope = form || document;
const selectors = [
"input[type='text']",
"input[type='email']",
"input[type='tel']",
"input:not([type='password']):not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio'])",
];
for (const sel of selectors) {
const inputs = searchScope.querySelectorAll(sel);
for (const input of inputs) {
if (input.offsetParent !== null || input.offsetWidth > 0) {
const name = (input.name || "").toLowerCase();
const id = (input.id || "").toLowerCase();
const placeholder = (input.placeholder || "").toLowerCase();
const autocomplete = (input.getAttribute("autocomplete") || "").toLowerCase();
if (
name.includes("user") || name.includes("name") || name.includes("account") ||
name.includes("email") || name.includes("login") || name.includes("mail") ||
id.includes("user") || id.includes("name") || id.includes("account") ||
id.includes("email") || id.includes("login") ||
placeholder.includes("user") || placeholder.includes("name") ||
placeholder.includes("email") || placeholder.includes("account") ||
placeholder.includes("登录") || placeholder.includes("账号") ||
placeholder.includes("用户") || placeholder.includes("邮箱") ||
autocomplete.includes("username") || autocomplete.includes("email")
) {
return input;
}
}
}
}
const allInputs = searchScope.querySelectorAll(
"input[type='text'], input[type='email'], input:not([type='password']):not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio'])"
);
for (const input of allInputs) {
if (input.offsetParent !== null || input.offsetWidth > 0) {
return input;
}
}
return null;
}
function findPasswordField(config) {
if (config.passwordSelector) {
const el = document.querySelector(config.passwordSelector);
if (el) return el;
}
const passwordInputs = document.querySelectorAll("input[type='password']");
for (const input of passwordInputs) {
if (input.offsetParent !== null || input.offsetWidth > 0) {
return input;
}
}
return passwordInputs.length > 0 ? passwordInputs[0] : null;
}
function findSubmitButton(config, passwordField) {
if (config.submitSelector) {
const els = document.querySelectorAll(config.submitSelector);
if (els.length === 1) return els[0];
if (els.length > 1) {
for (const el of els) {
if (el.offsetParent === null && el.offsetWidth === 0) continue;
const compact = (el.textContent || el.value || "").toLowerCase().replace(/\s/g, "");
if (compact.includes("登录") || compact.includes("login") || compact.includes("signin")) {
return el;
}
}
for (const el of els) {
if (el.offsetParent !== null || el.offsetWidth > 0) return el;
}
}
}
const form = passwordField ? passwordField.closest("form") : null;
const searchScope = form || document;
const submitSelectors = [
"button[type='submit']",
"input[type='submit']",
"button[type='button']",
"input[type='button']",
];
for (const sel of submitSelectors) {
const btns = searchScope.querySelectorAll(sel);
for (const btn of btns) {
if (btn.offsetParent !== null || btn.offsetWidth > 0) {
const text = (btn.textContent || btn.value || "").toLowerCase().trim();
const compact = text.replace(/\s/g, "");
if (
compact.includes("登录") || compact.includes("login") || compact.includes("login") ||
compact.includes("signin") || compact.includes("提交") || compact.includes("submit") ||
compact.includes("确定") || compact.includes("ok") || compact.includes("go")
) {
return btn;
}
}
}
}
for (const sel of submitSelectors) {
const btn = searchScope.querySelector(sel);
if (btn && (btn.offsetParent !== null || btn.offsetWidth > 0)) {
return btn;
}
}
const allButtons = searchScope.querySelectorAll("button, input[type='button'], a[role='button']");
for (const btn of allButtons) {
if (btn.offsetParent !== null || btn.offsetWidth > 0) {
const text = (btn.textContent || btn.value || "").toLowerCase().trim();
const compact = text.replace(/\s/g, "");
if (
compact.includes("登录") || compact.includes("login") || compact.includes("signin")
) {
return btn;
}
}
}
return null;
}
function fillForm(config, canSubmit = true) {
const passwordField = findPasswordField(config);
if (!passwordField) {
console.log("[AutoLogin]", t("logNoPasswordField"));
return false;
}
const usernameField = findUsernameField(config);
if (usernameField) {
setNativeValue(usernameField, config.username);
console.log("[AutoLogin]", t("logFilledUsername"));
} else {
console.log("[AutoLogin]", t("logNoUsernameField"));
}
setNativeValue(passwordField, config.password);
console.log("[AutoLogin]", t("logFilledPassword"));
// 检测到验证码则强制不自动提交,提示用户手动操作
const captcha = detectCaptcha();
let actualCanSubmit = canSubmit;
if (captcha && config.autoSubmit) {
actualCanSubmit = false;
console.warn("[AutoLogin]", t("logCaptchaSkip", captcha.reason));
showCaptchaToast(captcha.reason);
}
if (config.autoSubmit && actualCanSubmit) {
const submitBtn = findSubmitButton(config, passwordField);
if (submitBtn) {
// delayMs 给需要 JS 初始化的页面留出时间
const delay = Math.max(300, Number(config.delayMs) || 300);
setTimeout(() => {
console.log("[AutoLogin]", t("logAutoClick", String(delay)));
submitBtn.click();
}, delay);
return true;
}
if (passwordField.form) {
const delay = Math.max(300, Number(config.delayMs) || 300);
console.log("[AutoLogin]", t("logTryFormSubmit"));
setTimeout(() => {
passwordField.form.requestSubmit
? passwordField.form.requestSubmit()
: passwordField.form.submit();
}, delay);
return true;
}
console.log("[AutoLogin]", t("logNoSubmitAndForm"));
} else if (config.autoSubmit && !actualCanSubmit && captcha) {
console.warn("[AutoLogin]", t("logCaptchaFilled"));
} else if (config.autoSubmit && !canSubmit) {
console.warn("[AutoLogin]", t("logAttemptExceeded"));
} else {
console.log("[AutoLogin]", t("logNoAutoSubmit"));
}
return true;
}
// 验证码提示浮窗,8 秒后自动消失
let captchaToastEl = null;
function showCaptchaToast(reason) {
if (captchaToastEl && captchaToastEl.parentNode) {
captchaToastEl.parentNode.removeChild(captchaToastEl);
}
const el = document.createElement("div");
el.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
z-index: 2147483647;
background: #1a1a2e;
border: 1px solid #e94560;
border-radius: 8px;
padding: 10px 14px;
color: #e0e0e0;
font-size: 13px;
font-family: "Segoe UI","Microsoft YaHei",sans-serif;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
max-width: 300px;
`;
el.innerHTML = `<div style="font-weight:600;color:#e94560;margin-bottom:4px;">${t("captchaToastTitle")}</div>` +
`<div style="font-size:11px;color:#a0a0b0;">${t("captchaToastBody", reason)}</div>`;
document.body.appendChild(el);
captchaToastEl = el;
setTimeout(() => {
if (el && el.parentNode) el.parentNode.removeChild(el);
captchaToastEl = null;
}, 8000);
}
const MAX_LOGIN_ATTEMPTS = 3;
const ATTEMPT_COOLDOWN = 5 * 60 * 1000;
// 失败计数按 envId+domain 索引,同 domain 在不同环境独立计数
function attemptKey(envId, domain) {
return `${envId}::${domain}`;
}
async function getDomainAttempts(envId, domain) {
const key = attemptKey(envId, domain);
const data = await safeStorageGet("local", "loginAttempts");
const attempts = data.loginAttempts || {};
return attempts[key] || { count: 0, lastAttempt: 0 };
}
async function incrementDomainAttempts(envId, domain) {
const key = attemptKey(envId, domain);
const data = await safeStorageGet("local", "loginAttempts");
const attempts = data.loginAttempts || {};
const cur = attempts[key] || { count: 0, lastAttempt: 0 };
cur.count += 1;
cur.lastAttempt = Date.now();
attempts[key] = cur;
await safeStorageSet("local", { loginAttempts: attempts });
return cur;
}
async function resetDomainAttempts(envId, domain) {
const key = attemptKey(envId, domain);
const data = await safeStorageGet("local", "loginAttempts");
const attempts = data.loginAttempts || {};
if (attempts[key]) {
delete attempts[key];
await safeStorageSet("local", { loginAttempts: attempts });
}
}
async function loadAllConfigs() {
const data = await safeStorageGet("session", "environments");
const environments = data.environments || [];
const all = [];
environments.forEach((env) => {
(env.configs || []).forEach((cfg) => {
all.push({ ...cfg, envId: env.id, envName: env.name });
});
});
return all;
}
async function autoFillOnLoad() {
const allConfigs = await loadAllConfigs();
if (allConfigs.length === 0) return;
// 每个配置有独立开关 enabled
const enabledConfigs = allConfigs.filter((c) => c.enabled !== false);
if (enabledConfigs.length === 0) return;
const matched = enabledConfigs.filter((c) => isDomainMatch(window.location.href, c.domain));
if (matched.length === 0) return;
if (matched.length === 1) {
await fillSingleConfig(matched[0]);
return;
}
// 多环境命中:弹浮窗让用户选择
console.log("[AutoLogin]", t("logMatchedMulti", String(matched.length)));
showEnvPicker(matched);
}
// SPA 表单可能晚于脚本加载,填不到时每 500ms 重试,30 秒超时
const SPA_FILL_TIMEOUT_MS = 30000;
const SPA_RETRY_INTERVAL_MS = 500;
async function fillSingleConfig(config) {
console.log("[AutoLogin]", t("logStartFillEnv", config.envName || t("defaultEnvName")));
// 超限后仅填充不自动提交
let canSubmit = config.autoSubmit;
if (config.autoSubmit) {
const attempts = await getDomainAttempts(config.envId, config.domain);
const now = Date.now();
if (now - attempts.lastAttempt > ATTEMPT_COOLDOWN) {
attempts.count = 0;
}
if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
canSubmit = false;
console.warn(
"[AutoLogin] " + t(
"logAttemptExceededDetail",
config.envName,
config.domain,
String(attempts.count),
String(MAX_LOGIN_ATTEMPTS),
String(Math.ceil(ATTEMPT_COOLDOWN / 60000))
)
);
} else {
await incrementDomainAttempts(config.envId, config.domain);
}
}
// SPA 重试填充,30 秒超时
const startTime = Date.now();
const tryFill = () => {
const elapsed = Date.now() - startTime;
if (elapsed > SPA_FILL_TIMEOUT_MS) {
console.warn("[AutoLogin]", t("logSpaTimeout", String(SPA_FILL_TIMEOUT_MS / 1000)));
return;
}
const filled = fillForm(config, canSubmit);
if (!filled) {
setTimeout(tryFill, SPA_RETRY_INTERVAL_MS);
}
};
tryFill();
}
// 多环境命中时在页面右上角插入选择浮窗
let envPickerEl = null;
function showEnvPicker(configs) {
if (envPickerEl && envPickerEl.parentNode) {
envPickerEl.parentNode.removeChild(envPickerEl);
envPickerEl = null;
}
const overlay = document.createElement("div");
overlay.id = "auto-login-env-picker";
overlay.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
z-index: 2147483647;
background: #1a1a2e;
border: 1px solid #e94560;
border-radius: 10px;
padding: 14px 16px;
min-width: 240px;
max-width: 320px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
font-family: "Segoe UI","Microsoft YaHei",sans-serif;
color: #e0e0e0;
font-size: 13px;
`;
const title = document.createElement("div");
title.textContent = t("envPickerTitle");
title.style.cssText = "font-weight:600;color:#e94560;margin-bottom:10px;font-size:14px;";
overlay.appendChild(title);
const list = document.createElement("div");
list.style.cssText = "display:flex;flex-direction:column;gap:6px;";
configs.forEach((cfg) => {
const btn = document.createElement("button");
btn.style.cssText = `
text-align:left;
background:#16213e;
border:1px solid #0f3460;
border-radius:6px;
padding:8px 10px;
color:#e0e0e0;
cursor:pointer;
font-size:12px;
font-family:inherit;
transition:background 0.15s;
`;
const envLabel = escapeHtml(cfg.envName || t("defaultEnvName"));
const domainLabel = escapeHtml(cfg.alias || cfg.domain);
const userLabel = escapeHtml(cfg.username);
btn.innerHTML = `
<div style="font-weight:600;color:#e94560;">${envLabel}</div>
<div style="color:#888;font-size:11px;margin-top:2px;">${domainLabel} · ${userLabel}</div>
`;
btn.addEventListener("mouseenter", () => {
btn.style.background = "#0f3460";
});
btn.addEventListener("mouseleave", () => {
btn.style.background = "#16213e";
});
btn.addEventListener("click", async () => {
overlay.parentNode && overlay.parentNode.removeChild(overlay);
envPickerEl = null;
await fillSingleConfig(cfg);
});
list.appendChild(btn);
});
overlay.appendChild(list);
const closeBtn = document.createElement("button");
closeBtn.textContent = t("cancelBtn");
closeBtn.style.cssText = `
margin-top:10px;
width:100%;
padding:6px;
background:transparent;
border:1px solid #0f3460;
border-radius:6px;
color:#888;
cursor:pointer;
font-size:11px;
font-family:inherit;
`;
closeBtn.addEventListener("click", () => {
overlay.parentNode && overlay.parentNode.removeChild(overlay);
envPickerEl = null;
});
overlay.appendChild(closeBtn);
document.body.appendChild(overlay);
envPickerEl = overlay;
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "manualFill" && message.config) {
// 手动触发填充,重置失败计数
resetDomainAttempts(message.config.envId, message.config.domain);
const filled = fillForm(message.config, true);
sendResponse({ success: filled });
} else if (message.action === "manualFillMulti" && Array.isArray(message.configs)) {
showEnvPicker(message.configs);
sendResponse({ success: true });
}
return true;
});
autoFillOnLoad();
// URL 变化后若新页面不再匹配任何配置,重置之前匹配环境的失败计数
let lastUrl = window.location.href;
const observer = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
const prevUrl = lastUrl;
lastUrl = window.location.href;
loadAllConfigs().then((allConfigs) => {
const stillMatched = allConfigs.some((c) => isDomainMatch(window.location.href, c.domain));
if (!stillMatched) {
const prevMatched = allConfigs.filter((c) => isDomainMatch(prevUrl, c.domain));
prevMatched.forEach((c) => resetDomainAttempts(c.envId, c.domain));
}
});
setTimeout(autoFillOnLoad, 1000);
}
});
observer.observe(document.body, { childList: true, subtree: true });