// 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 = `
需要手动操作
` + `
${reason},已为你填充账密,请完成验证码后点击登录
`; 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 = `
${envLabel}
${domainLabel} · ${userLabel}
`; 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 });