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) { let searchScope = config.usernameSelector ? document.querySelector(config.usernameSelector) : null; if (!searchScope) { const passwordField = findPasswordField(config); if (!passwordField) return null; searchScope = passwordField.closest("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) { const scope = config.passwordSelector ? (document.querySelector(config.passwordSelector) || document) : document; const passwordInputs = scope.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) { let searchScope = config.submitSelector ? document.querySelector(config.submitSelector) : null; if (!searchScope) { const form = passwordField ? passwordField.closest("form") : null; 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 = `
${t("captchaToastTitle")}
` + `
${t("captchaToastBody", 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 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 = `
${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 = 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 });