Files
auto-login/content.js
T
2026-09-14 13:45:17 +08:00

529 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// isDomainMatch 由 utils.js 提供(全局函数)
// 2FA / 验证码检测:检测到则不自动提交,仅填充
function detectCaptcha() {
// 1. reCAPTCHA
if (document.querySelector(".g-recaptcha, [data-sitekey], iframe[src*='recaptcha'], iframe[src*='google.com/recaptcha']")) {
return { type: "reCAPTCHA", reason: "检测到 reCAPTCHA" };
}
// 2. hCaptcha
if (document.querySelector(".h-captcha, iframe[src*='hcaptcha']")) {
return { type: "hCaptcha", reason: "检测到 hCaptcha" };
}
// 3. 国内常见滑块/验证码容器
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: `检测到验证码元素 (${sel})` };
}
} catch { /* selector 可能无效 */ }
}
// 4. 明显的"验证码"文字输入框(带图形码)
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: "检测到验证码输入框" };
}
}
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 el = document.querySelector(config.submitSelector);
if (el) 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();
if (
text.includes("登录") || text.includes("login") || text.includes("log in") ||
text.includes("sign in") || text.includes("提交") || text.includes("submit") ||
text.includes("确定") || text.includes("ok") || text.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();
if (
text.includes("登录") || text.includes("login") || text.includes("log in") ||
text.includes("sign in")
) {
return btn;
}
}
}
return null;
}
function fillForm(config, canSubmit = true) {
const passwordField = findPasswordField(config);
if (!passwordField) {
console.log("[AutoLogin] 未找到密码输入框");
return false;
}
const usernameField = findUsernameField(config);
if (usernameField) {
setNativeValue(usernameField, config.username);
console.log("[AutoLogin] 已填充用户名");
} else {
console.log("[AutoLogin] 未找到用户名输入框,跳过用户名填充");
}
setNativeValue(passwordField, config.password);
console.log("[AutoLogin] 已填充密码");
// 验证码检测:检测到则强制不自动提交,提示用户手动操作
const captcha = detectCaptcha();
let actualCanSubmit = canSubmit;
if (captcha && config.autoSubmit) {
actualCanSubmit = false;
console.warn(`[AutoLogin] ${captcha.reason},已跳过自动提交,请手动完成验证码后点击登录`);
// 在页面右上角显示提示浮窗
showCaptchaToast(captcha.reason);
}
if (config.autoSubmit && actualCanSubmit) {
const submitBtn = findSubmitButton(config, passwordField);
if (submitBtn) {
// 延迟提交:config.delayMs 给某些需要 JS 初始化的页面留时间
const delay = Math.max(300, Number(config.delayMs) || 300);
setTimeout(() => {
console.log(`[AutoLogin] 自动点击登录按钮(延迟 ${delay}ms)`);
submitBtn.click();
}, delay);
return true;
}
if (passwordField.form) {
const delay = Math.max(300, Number(config.delayMs) || 300);
console.log("[AutoLogin] 未找到登录按钮,尝试提交表单");
setTimeout(() => {
passwordField.form.requestSubmit
? passwordField.form.requestSubmit()
: passwordField.form.submit();
}, delay);
return true;
}
console.log("[AutoLogin] 未找到登录按钮,也无法提交表单");
} else if (config.autoSubmit && !actualCanSubmit && captcha) {
console.warn("[AutoLogin] 已检测到验证码,仅填充不自动提交,请手动完成验证码后点击登录");
} else if (config.autoSubmit && !canSubmit) {
console.warn("[AutoLogin] 自动登录失败次数已达上限,仅填充不自动提交,请手动确认后点击登录");
} else {
console.log("[AutoLogin] 自动提交已关闭,仅填充");
}
return true;
}
// 验证码提示浮窗:在页面右上角显示,几秒后自动消失
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;">需要手动操作</div>` +
`<div style="font-size:11px;color:#a0a0b0;">${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 chrome.storage.local.get("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 chrome.storage.local.get("loginAttempts");
const attempts = data.loginAttempts || {};
const cur = attempts[key] || { count: 0, lastAttempt: 0 };
cur.count += 1;
cur.lastAttempt = Date.now();
attempts[key] = cur;
await chrome.storage.local.set({ loginAttempts: attempts });
return cur;
}
async function resetDomainAttempts(envId, domain) {
const key = attemptKey(envId, domain);
const data = await chrome.storage.local.get("loginAttempts");
const attempts = data.loginAttempts || {};
if (attempts[key]) {
delete attempts[key];
await chrome.storage.local.set({ loginAttempts: attempts });
}
}
// 从 session 中扁平化读取所有配置(解锁后明文存于 session,未解锁时为空)
async function loadAllConfigs() {
const data = await chrome.storage.session.get("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 === false 的配置(独立开关)
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] 匹配到 ${matched.length} 个环境配置,弹出选择菜单`);
showEnvPicker(matched);
}
// SPA 动态表单:尝试填充,表单未出现时持续重试 30 秒
const SPA_FILL_TIMEOUT_MS = 30000;
const SPA_RETRY_INTERVAL_MS = 500;
async function fillSingleConfig(config) {
console.log(`[AutoLogin] 使用环境「${config.envName || "默认"}」的配置开始填充`);
// 检查自动登录尝试次数,超过上限则仅填充不自动提交
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] 环境「${config.envName}」域名 ${config.domain} 自动登录已失败 ${attempts.count} 次,` +
`达到上限 ${MAX_LOGIN_ATTEMPTS} 次,停止自动提交。` +
`请手动确认账号密码后点击登录,或 ${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] SPA 动态表单等待超时(${SPA_FILL_TIMEOUT_MS / 1000}s),已停止尝试`);
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 = "选择要使用哪个环境";
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 || "默认");
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 = "取消";
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;
}
// escapeHtml 由 utils.js 提供(全局函数)
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)) {
// popup 多环境命中:弹出选择菜单
showEnvPicker(message.configs);
sendResponse({ success: true });
}
return true;
});
autoFillOnLoad();
let lastUrl = window.location.href;
const observer = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
const prevUrl = lastUrl;
lastUrl = window.location.href;
// URL 变化后,如果新页面不再匹配任何配置,重置之前匹配环境的失败计数
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 });