diff --git a/dist/background.js b/dist/background.js deleted file mode 100644 index 38d1762..0000000 --- a/dist/background.js +++ /dev/null @@ -1,244 +0,0 @@ -"use strict"; -importScripts("crypto-store.js"); -let masterKey = null; // 解锁后持有,锁定或 SW 重启后清空 -let autoLockTimer = null; -const AUTO_LOCK_MS = 10 * 60 * 1000; // 10 分钟无操作自动锁定 -chrome.runtime.onInstalled.addListener(() => { - chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) - .catch((err) => console.warn("[AutoLogin] 设置 sidePanel 行为失败:", err)); - migrateLegacyVault(); - console.log("[AutoLogin] 扩展已安装"); -}); -chrome.action.onClicked.addListener((tab) => { - if (tab?.id !== undefined) { - chrome.sidePanel.open({ tabId: tab.id }).catch(() => { }); - } -}); -// 旧版明文数据迁移:把 loginConfigs 转为 environments 明文结构 -async function migrateLegacyVault() { - const data = await chrome.storage.local.get(["environments", "loginConfigs", "vaultMeta"]); - if (data.vaultMeta) - return; - if (Array.isArray(data.environments) && data.environments.length > 0) - return; - if (Array.isArray(data.loginConfigs) && data.loginConfigs.length > 0) { - const envs = [{ id: "env_legacy", name: "默认", configs: data.loginConfigs }]; - await chrome.storage.local.set({ environments: envs }); - await chrome.storage.local.remove("loginConfigs"); - console.log("[AutoLogin] 旧版 loginConfigs 已迁移为明文 environments"); - } -} -// 无 vault 明文模式:确保 session 有 environments(从 local 加载或创建预设) -async function ensurePlainEnvironments() { - if (await CryptoStore.hasVault()) - return; // 加密模式不处理 - const data = await chrome.storage.local.get("environments"); - let envs = data.environments; - if (!Array.isArray(envs) || envs.length === 0) { - const PRESET = ["个人", "测试", "生产"]; - envs = PRESET.map((name) => ({ - id: "env_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8), - name, - configs: [], - })); - await chrome.storage.local.set({ environments: envs }); - } - const session = await chrome.storage.session.get("environments"); - if (!session.environments) { - await chrome.storage.session.set({ - environments: envs, - activeEnvId: envs[0]?.id || null, - }); - } -} -// SW 启动即初始化明文环境到 session,供 content script 自动登录读取 -ensurePlainEnvironments().catch((err) => console.warn("[AutoLogin] 启动初始化失败:", err)); -autoRestoreUnlock().catch((err) => console.warn("[AutoLogin] 自动恢复解锁失败:", err)); -function resetAutoLockTimer() { - if (autoLockTimer) - clearTimeout(autoLockTimer); - if (masterKey) { - autoLockTimer = setTimeout(() => { - console.log("[AutoLogin] 闲置超时,自动锁定"); - doLock(); - }, AUTO_LOCK_MS); - } -} -async function doLock() { - masterKey = null; - if (autoLockTimer) { - clearTimeout(autoLockTimer); - autoLockTimer = null; - } - await CryptoStore.lock(); - await chrome.storage.session.remove("__autoPwd"); -} -// SW 重启后自动恢复解锁:从 session 取已存密码解锁 vault -async function autoRestoreUnlock() { - try { - if (!(await CryptoStore.hasVault())) - return; - if (masterKey) - return; - const { __autoPwd } = await chrome.storage.session.get("__autoPwd"); - if (!__autoPwd) - return; - const { key } = await CryptoStore.unlock(__autoPwd); - masterKey = key; - resetAutoLockTimer(); - console.log("[AutoLogin] 自动恢复解锁成功"); - } - catch (err) { - await chrome.storage.session.remove("__autoPwd").catch(() => { }); - } -} -// session 中 environments 变化时用内存密钥重新加密写回 local -chrome.storage.onChanged.addListener(async (changes, areaName) => { - if (areaName !== "session") - return; - if (!changes.environments) - return; - const environments = changes.environments.newValue; - if (!environments) - return; // 锁定时被清除,无需持久化 - try { - if (masterKey) { - await CryptoStore.persistEncrypted(environments, masterKey); - } - else { - // 无 vault 明文模式:明文回写 local - await chrome.storage.local.set({ environments }); - } - } - catch (err) { - console.warn("[AutoLogin] 持久化失败:", err); - } -}); -chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { - (async () => { - try { - switch (message.action) { - case "getVaultStatus": { - const hasVault = await CryptoStore.hasVault(); - if (!hasVault) { - await ensurePlainEnvironments(); - const local = await chrome.storage.local.get("environments"); - const sess = await chrome.storage.session.get("environments"); - const cfgCount = (arr) => (arr || []).reduce((s, e) => s + (e.configs?.length || 0), 0); - sendResponse({ - hasVault: false, unlocked: true, pendingImport: false, - localEnvs: (local.environments || []).length, localCfgs: cfgCount(local.environments), - sessEnvs: (sess.environments || []).length, sessCfgs: cfgCount(sess.environments), - }); - break; - } - if (!masterKey) - await autoRestoreUnlock(); - sendResponse({ hasVault, unlocked: !!masterKey, pendingImport: false }); - break; - } - case "loadAllConfigs": { - if (!(await CryptoStore.hasVault())) - await ensurePlainEnvironments(); - 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 })); - }); - sendResponse({ configs: all }); - break; - } - case "getDomainAttempts": { - const key = `${message.envId}::${message.domain}`; - const data = await chrome.storage.local.get("loginAttempts"); - const attempts = data.loginAttempts || {}; - sendResponse({ attempts: attempts[key] || { count: 0, lastAttempt: 0 } }); - break; - } - case "incrementDomainAttempts": { - const key = `${message.envId}::${message.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 }); - sendResponse({ attempts: cur }); - break; - } - case "resetDomainAttempts": { - const key = `${message.envId}::${message.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 }); - } - sendResponse({ success: true }); - break; - } - case "setupVault": { - // 设置口令:若已有明文 environments 则加密它,否则创建预设 - const localData = await chrome.storage.local.get("environments"); - let initialEnvs; - if (Array.isArray(localData.environments) && localData.environments.length > 0) { - initialEnvs = localData.environments; - } - else { - const PRESET = ["个人", "测试", "生产"]; - initialEnvs = PRESET.map((name) => ({ - id: "env_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8), - name, - configs: [], - })); - } - masterKey = await CryptoStore.setupVault(message.password, initialEnvs); - await chrome.storage.session.set({ __autoPwd: message.password }); - await chrome.storage.local.remove("environments"); // 已加密,明文不再需要 - resetAutoLockTimer(); - sendResponse({ success: true }); - break; - } - case "unlock": { - try { - const { key } = await CryptoStore.unlock(message.password); - masterKey = key; - await chrome.storage.session.set({ __autoPwd: message.password }); - resetAutoLockTimer(); - sendResponse({ success: true }); - } - catch (err) { - sendResponse({ success: false, error: err.message }); - } - break; - } - case "lock": { - await doLock(); - sendResponse({ success: true }); - break; - } - case "getActivity": { - sendResponse({ - unlocked: !!masterKey, - autoLockMs: AUTO_LOCK_MS, - }); - break; - } - case "ping": { - // 任何消息都视为用户活动,顺带重置自动锁定计时 - resetAutoLockTimer(); - sendResponse({ pong: true }); - break; - } - default: - sendResponse({ error: "unknown action" }); - } - } - catch (err) { - sendResponse({ success: false, error: err.message }); - } - })(); - return true; // 异步响应 -}); diff --git a/dist/content.js b/dist/content.js deleted file mode 100644 index 84c3b6a..0000000 --- a/dist/content.js +++ /dev/null @@ -1,548 +0,0 @@ -"use strict"; -const t = (key, ...args) => chrome.i18n.getMessage(key, args) || key; -const _sendMessage = chrome.runtime.sendMessage; -// 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", - "[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 Array.from(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 deepQuerySelectorAll(selector, root) { - const result = []; - const stack = [root || document]; - while (stack.length) { - const r = stack.pop(); - result.push(...r.querySelectorAll(selector)); - for (const el of r.querySelectorAll("*")) { - if (el.shadowRoot) - stack.push(el.shadowRoot); - } - } - return result; -} -function deepQuerySelector(selector, root) { - return deepQuerySelectorAll(selector, root)[0] || null; -} -function resolveElement(value, multiple) { - if (!value) - return null; - const trimmed = value.trim(); - if (!trimmed.startsWith("<")) { - return multiple ? deepQuerySelectorAll(trimmed) : deepQuerySelector(trimmed); - } - const doc = new DOMParser().parseFromString(trimmed, "text/html"); - const el = doc.body.firstElementChild; - if (!el) - return null; - const tag = el.tagName.toLowerCase(); - const id = el.id; - const cls = Array.from(el.classList); - const type = el.getAttribute("type"); - const nameAttr = el.getAttribute("name"); - const candidates = []; - if (id) - candidates.push(`${tag}#${CSS.escape(id)}`); - if (cls.length) - candidates.push(`${tag}.${cls.map(CSS.escape).join(".")}`); - if (type) - candidates.push(`${tag}[type="${type}"]`); - if (nameAttr) - candidates.push(`${tag}[name="${CSS.escape(nameAttr)}"]`); - for (const c of cls) - candidates.push(`${tag}.${CSS.escape(c)}`); - candidates.push(tag); - for (const sel of candidates) { - if (multiple) { - const els = deepQuerySelectorAll(sel); - if (els.length) - return els; - } - else { - const found = deepQuerySelector(sel); - if (found) - return found; - } - } - return null; -} -function findUsernameField(config) { - if (config.usernameSelector) { - const el = resolveElement(config.usernameSelector); - if (el) - return el; - } - const passwordField = findPasswordField(config); - if (!passwordField) - return null; - const 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 = deepQuerySelectorAll(sel, searchScope); - 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 = deepQuerySelectorAll("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'])", searchScope); - for (const input of allInputs) { - if (input.offsetParent !== null || input.offsetWidth > 0) { - return input; - } - } - return null; -} -function findPasswordField(config) { - if (config.passwordSelector) { - const el = resolveElement(config.passwordSelector); - if (el) - return el; - } - const passwordInputs = deepQuerySelectorAll("input[type='password']"); - for (const input of passwordInputs) { - if (input.offsetParent !== null || input.offsetWidth > 0) { - return input; - } - } - if (passwordInputs.length > 0) - return passwordInputs[0]; - const allInputs = deepQuerySelectorAll("input"); - for (const input of allInputs) { - if (input.type === "password") - return input; - const sig = ((input.name || "") + " " + (input.id || "") + " " + - (input.placeholder || "") + " " + (input.getAttribute("autocomplete") || "")).toLowerCase(); - if (sig.includes("password") || sig.includes("passwd") || sig.includes("pwd")) { - if (input.offsetParent !== null || input.offsetWidth > 0) - return input; - } - } - return null; -} -function findSubmitButton(config, passwordField) { - if (config.submitSelector) { - const els = resolveElement(config.submitSelector, true); - if (els && els.length === 1) - return els[0]; - if (els && 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 = deepQuerySelectorAll(sel, searchScope); - 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 = deepQuerySelector(sel, searchScope); - if (btn && (btn.offsetParent !== null || btn.offsetWidth > 0)) { - return btn; - } - } - const allButtons = deepQuerySelectorAll("button, input[type='button'], a[role='button']", searchScope); - 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; -async function getDomainAttempts(envId, domain) { - const resp = await _sendMessage({ action: "getDomainAttempts", envId, domain }).catch(() => null); - return resp?.attempts || { count: 0, lastAttempt: 0 }; -} -async function incrementDomainAttempts(envId, domain) { - const resp = await _sendMessage({ action: "incrementDomainAttempts", envId, domain }).catch(() => null); - return resp?.attempts || { count: 0, lastAttempt: 0 }; -} -async function resetDomainAttempts(envId, domain) { - await _sendMessage({ action: "resetDomainAttempts", envId, domain }).catch(() => { }); -} -async function loadAllConfigs() { - const resp = await _sendMessage({ action: "loadAllConfigs" }).catch(() => null); - return resp?.configs || []; -} -async function autoFillOnLoad() { - let allConfigs = await loadAllConfigs(); - if (allConfigs.length === 0) { - // session 可能为空(明文模式 SW 重启后未初始化),触发 background 加载后重试 - try { - await _sendMessage({ action: "getVaultStatus" }); - allConfigs = await loadAllConfigs(); - } - catch (e) { - console.warn("[AutoLogin] 初始化失败:", e.message || e); - return; - } - 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) { - const inputs = deepQuerySelectorAll("input"); - const pwd = deepQuerySelectorAll("input[type='password']"); - console.warn("[AutoLogin] 填充超时: input=" + inputs.length + " pwd=" + pwd.length + " form=" + document.querySelectorAll("form").length + " | " + inputs.slice(0, 6).map((i) => "type=" + (i.type || "?") + "/name=" + (i.name || i.id || "?")).join(", ")); - 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); - if (filled) { - sendResponse({ success: true }); - } - else { - const inputs = deepQuerySelectorAll("input"); - const pwdInputs = deepQuerySelectorAll("input[type='password']"); - sendResponse({ - success: false, - diag: { - total: inputs.length, - pwd: pwdInputs.length, - forms: document.querySelectorAll("form").length, - sample: inputs.slice(0, 10).map((i) => `type=${i.type || "?"}|name=${(i.name || i.id || i.placeholder || "?").slice(0, 20)}`), - }, - }); - } - } - 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 }); diff --git a/dist/crypto-file.js b/dist/crypto-file.js deleted file mode 100644 index bb1f810..0000000 --- a/dist/crypto-file.js +++ /dev/null @@ -1,47 +0,0 @@ -const enc = new TextEncoder(); -const dec = new TextDecoder(); -export function bufToBase64(buf) { - const bytes = new Uint8Array(buf); - let bin = ""; - for (let i = 0; i < bytes.length; i++) - bin += String.fromCharCode(bytes[i]); - return btoa(bin); -} -export function base64ToBuf(b64) { - const bin = atob(b64); - const bytes = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) - bytes[i] = bin.charCodeAt(i); - return bytes.buffer; -} -async function deriveKey(password, salt, iterations) { - const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]); - return crypto.subtle.deriveKey({ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); -} -export async function encryptConfigs(configs, password) { - const salt = crypto.getRandomValues(new Uint8Array(16)); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const iterations = 100000; - const key = await deriveKey(password, salt, iterations); - const plaintext = enc.encode(JSON.stringify(configs)); - const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext); - return { - algorithm: "AES-GCM", - iv: bufToBase64(iv), - ciphertext: bufToBase64(ciphertext), - kdf: { - algorithm: "PBKDF2", - iterations, - hash: "SHA-256", - salt: bufToBase64(salt) - } - }; -} -export async function decryptConfigs(cipher, password) { - const salt = new Uint8Array(base64ToBuf(cipher.kdf.salt)); - const iv = new Uint8Array(base64ToBuf(cipher.iv)); - const iterations = cipher.kdf.iterations || 100000; - const key = await deriveKey(password, salt, iterations); - const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, base64ToBuf(cipher.ciphertext)); - return JSON.parse(dec.decode(plainBuf)); -} diff --git a/dist/crypto-store.js b/dist/crypto-store.js deleted file mode 100644 index 062f38d..0000000 --- a/dist/crypto-store.js +++ /dev/null @@ -1,159 +0,0 @@ -"use strict"; -/** - * 加密库:主口令 + AES-GCM 整库加密。 - * local 存 vaultMeta / encryptedVault;解锁后明文放 session,不进 local; - * 派生密钥仅由 background 在内存中持有。 - */ -(function (root, factory) { - if (typeof module === "object" && module.exports) { - module.exports = factory({ - getCrypto: () => globalThis.crypto, - getStorage: (area) => { - const c = typeof chrome !== "undefined" ? chrome : null; - return c?.storage?.[area] || null; - }, - }); - } - else { - const api = factory({ - getCrypto: () => crypto, - getStorage: (area) => chrome.storage[area], - }); - root.CryptoStore = api; - } -})(typeof globalThis !== "undefined" ? globalThis : this, function ({ getCrypto, getStorage }) { - const VAULT_META_KEY = "vaultMeta"; - const ENCRYPTED_VAULT_KEY = "encryptedVault"; - const SESSION_ENV_KEY = "environments"; - const SESSION_ACTIVE_KEY = "activeEnvId"; - const PBKDF2_ITERATIONS = 200000; - const VAULT_VERSION = 1; - function b64encode(buf) { - const bytes = new Uint8Array(buf); - let s = ""; - for (let i = 0; i < bytes.length; i++) - s += String.fromCharCode(bytes[i]); - return btoa(s); - } - function b64decode(str) { - const s = atob(str); - const bytes = new Uint8Array(s.length); - for (let i = 0; i < s.length; i++) - bytes[i] = s.charCodeAt(i); - return bytes; - } - function randomBytes(n) { - const arr = new Uint8Array(n); - getCrypto().getRandomValues(arr); - return arr; - } - async function deriveKey(password, salt, iterations) { - const enc = new TextEncoder(); - const keyMaterial = await getCrypto().subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey"]); - return getCrypto().subtle.deriveKey({ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); - } - async function encryptJson(obj, key) { - const enc = new TextEncoder(); - const iv = randomBytes(12); - const ciphertext = await getCrypto().subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(JSON.stringify(obj))); - return { iv: b64encode(iv), ciphertext: b64encode(ciphertext) }; - } - /** 解密为 JSON 对象,口令错误或数据损坏时抛错 */ - async function decryptJson({ iv, ciphertext }, key) { - const dec = new TextDecoder(); - const plain = await getCrypto().subtle.decrypt({ name: "AES-GCM", iv: b64decode(iv) }, key, b64decode(ciphertext)); - return JSON.parse(dec.decode(plain)); - } - async function getLocal(key) { - return (await getStorage("local").get(key))[key]; - } - async function hasVault() { - const meta = await getLocal(VAULT_META_KEY); - const cipher = await getLocal(ENCRYPTED_VAULT_KEY); - return !!(meta && cipher); - } - async function isUnlocked() { - const data = await getStorage("session").get(SESSION_ENV_KEY); - return !!data[SESSION_ENV_KEY]; - } - /** 首次设置口令:生成盐、加密初始环境写入 local,并把明文写入 session,返回派生密钥 */ - async function setupVault(password, environments) { - if (!password) - throw new Error("口令不能为空"); - const salt = randomBytes(16); - const key = await deriveKey(password, salt, PBKDF2_ITERATIONS); - const cipher = await encryptJson(environments || [], key); - const meta = { - version: VAULT_VERSION, - iterations: PBKDF2_ITERATIONS, - salt: b64encode(salt), - }; - await getStorage("local").set({ - [VAULT_META_KEY]: meta, - [ENCRYPTED_VAULT_KEY]: cipher, - }); - const envs = environments || []; - await getStorage("session").set({ - [SESSION_ENV_KEY]: envs, - [SESSION_ACTIVE_KEY]: envs[0]?.id || null, - }); - return key; - } - /** 解锁:解密 local 密文写入 session,口令错误抛错,返回明文与密钥 */ - async function unlock(password) { - const meta = await getLocal(VAULT_META_KEY); - const cipher = await getLocal(ENCRYPTED_VAULT_KEY); - if (!meta || !cipher) - throw new Error("尚未设置口令"); - const key = await deriveKey(password, b64decode(meta.salt), meta.iterations); - let environments; - try { - environments = await decryptJson(cipher, key); - } - catch { - throw new Error("口令错误"); - } - const active = (await getStorage("session").get(SESSION_ACTIVE_KEY))[SESSION_ACTIVE_KEY] - || environments[0]?.id || null; - await getStorage("session").set({ - [SESSION_ENV_KEY]: environments, - [SESSION_ACTIVE_KEY]: active, - }); - return { environments, key }; - } - async function lock() { - await getStorage("session").remove([SESSION_ENV_KEY, SESSION_ACTIVE_KEY]); - } - async function getVaultMeta() { - return getLocal(VAULT_META_KEY); - } - async function getEncryptedVault() { - return getLocal(ENCRYPTED_VAULT_KEY); - } - /** 用给定密钥加密 environments 并写回 local */ - async function persistEncrypted(environments, key) { - const cipher = await encryptJson(environments, key); - await getStorage("local").set({ [ENCRYPTED_VAULT_KEY]: cipher }); - } - return { - VAULT_META_KEY, - ENCRYPTED_VAULT_KEY, - SESSION_ENV_KEY, - SESSION_ACTIVE_KEY, - PBKDF2_ITERATIONS, - hasVault, - isUnlocked, - setupVault, - unlock, - lock, - deriveKey, - encryptJson, - decryptJson, - getVaultMeta, - getEncryptedVault, - persistEncrypted, - b64encode, - b64decode, - randomBytes, - }; -}); diff --git a/dist/env-store.js b/dist/env-store.js deleted file mode 100644 index ef215bf..0000000 --- a/dist/env-store.js +++ /dev/null @@ -1,136 +0,0 @@ -"use strict"; -/** - * 环境数据层:环境/配置的读写,数据存于 chrome.storage.session(解锁后明文)。 - * 首次建口令由 crypto-store.setupVault 完成;加密回写由 background 监听完成。 - */ -(function (root, factory) { - if (typeof module === "object" && module.exports) { - module.exports = factory({ - getStorage: () => (typeof chrome !== "undefined" ? chrome.storage.session : null), - getLocalStorage: () => (typeof chrome !== "undefined" ? chrome.storage.local : null), - }); - } - else { - const api = factory({ - getStorage: () => chrome.storage.session, - getLocalStorage: () => chrome.storage.local, - }); - root.EnvStore = api; - } -})(typeof globalThis !== "undefined" ? globalThis : this, function ({ getStorage, getLocalStorage }) { - const ENV_KEY = "environments"; - const ACTIVE_ENV_KEY = "activeEnvId"; - function genId() { - return "env_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); - } - function genCfgId() { - return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); - } - function storage() { - const s = getStorage(); - if (!s) - throw new Error("chrome.storage.session 不可用"); - return s; - } - async function loadEnvironments() { - const data = await storage().get([ENV_KEY, ACTIVE_ENV_KEY]); - const environments = data[ENV_KEY] || []; - const activeEnvId = data[ACTIVE_ENV_KEY] || environments[0]?.id || null; - return { environments, activeEnvId }; - } - async function saveEnvironments(environments, activeEnvId) { - const patch = { [ENV_KEY]: environments }; - if (activeEnvId !== undefined) - patch[ACTIVE_ENV_KEY] = activeEnvId; - await storage().set(patch); - } - async function getActiveEnv() { - const { environments, activeEnvId } = await loadEnvironments(); - return environments.find((e) => e.id === activeEnvId) || environments[0] || null; - } - async function setActiveEnv(envId) { - await storage().set({ [ACTIVE_ENV_KEY]: envId }); - } - const MAX_ENVIRONMENTS = 5; - async function createEnvironment(name) { - const { environments } = await loadEnvironments(); - if (environments.length >= MAX_ENVIRONMENTS) { - throw new Error(`最多 ${MAX_ENVIRONMENTS} 个环境`); - } - const env = { id: genId(), name, configs: [] }; - environments.push(env); - await saveEnvironments(environments, env.id); - return env; - } - async function renameEnvironment(envId, newName) { - const { environments } = await loadEnvironments(); - const env = environments.find((e) => e.id === envId); - if (env) { - env.name = newName; - await saveEnvironments(environments); - } - return env; - } - async function deleteEnvironment(envId) { - const { environments, activeEnvId } = await loadEnvironments(); - if (environments.length <= 1) { - throw new Error("至少保留一个环境"); - } - const idx = environments.findIndex((e) => e.id === envId); - if (idx === -1) - return; - environments.splice(idx, 1); - const newActive = activeEnvId === envId ? environments[0].id : activeEnvId; - await saveEnvironments(environments, newActive ?? undefined); - // 失败计数存于 local(非敏感),删除环境时一并清理 - const local = getLocalStorage(); - if (local) { - const attemptsData = await local.get("loginAttempts"); - const attempts = attemptsData.loginAttempts || {}; - Object.keys(attempts).forEach((k) => { - if (k.startsWith(envId + "::")) - delete attempts[k]; - }); - await local.set({ loginAttempts: attempts }); - } - } - async function loadConfigs() { - const env = await getActiveEnv(); - return env?.configs || []; - } - async function saveConfigs(configs) { - const { environments, activeEnvId } = await loadEnvironments(); - const env = environments.find((e) => e.id === activeEnvId); - if (!env) - throw new Error("未找到当前环境"); - env.configs = configs; - await saveEnvironments(environments); - } - async function loadAllConfigs() { - const { environments } = await loadEnvironments(); - const all = []; - environments.forEach((env) => { - (env.configs || []).forEach((cfg) => { - all.push({ ...cfg, envId: env.id, envName: env.name }); - }); - }); - return all; - } - return { - ENV_KEY, - ACTIVE_ENV_KEY, - MAX_ENVIRONMENTS, - loadEnvironments, - saveEnvironments, - getActiveEnv, - setActiveEnv, - createEnvironment, - renameEnvironment, - deleteEnvironment, - loadConfigs, - saveConfigs, - loadAllConfigs, - genId, - genCfgId, - }; -}); diff --git a/dist/i18n.js b/dist/i18n.js deleted file mode 100644 index 9327786..0000000 --- a/dist/i18n.js +++ /dev/null @@ -1,41 +0,0 @@ -export function t(key, ...args) { - const msg = chrome.i18n.getMessage(key, args); - return msg || key; -} -export function applyI18n(root = document) { - root.querySelectorAll("[data-i18n]").forEach((el) => { - const key = el.dataset.i18n || ""; - if (el.children.length === 0) { - const msg = chrome.i18n.getMessage(key); - if (msg) - el.textContent = msg; - } - else { - const msg = chrome.i18n.getMessage(key); - if (msg) { - const firstText = el.firstChild; - if (firstText && firstText.nodeType === Node.TEXT_NODE) { - firstText.nodeValue = msg; - } - } - } - }); - root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { - const key = el.dataset.i18nPlaceholder || ""; - const msg = chrome.i18n.getMessage(key); - if (msg) - el.placeholder = msg; - }); - root.querySelectorAll("[data-i18n-title]").forEach((el) => { - const key = el.dataset.i18nTitle || ""; - const msg = chrome.i18n.getMessage(key); - if (msg) - el.title = msg; - }); - root.querySelectorAll("[data-i18n-aria]").forEach((el) => { - const key = el.dataset.i18nAria || ""; - const msg = chrome.i18n.getMessage(key); - if (msg) - el.setAttribute("aria-label", msg); - }); -} diff --git a/dist/messaging.js b/dist/messaging.js deleted file mode 100644 index 6c30726..0000000 --- a/dist/messaging.js +++ /dev/null @@ -1,27 +0,0 @@ -import { t } from "./i18n.js"; -export function sendMessage(message, retries = 3) { - return new Promise((resolve, reject) => { - let attempt = 0; - const trySend = () => { - attempt++; - if (!chrome.runtime?.id) { - location.reload(); - reject(new Error(t("toastContextReload"))); - return; - } - chrome.runtime.sendMessage(message, (resp) => { - const err = chrome.runtime.lastError; - if (err) { - if (attempt < retries && /Receiving end does not exist/i.test(err.message ?? "")) { - setTimeout(trySend, 200 * attempt); - return; - } - reject(new Error(err.message ?? "")); - return; - } - resolve(resp); - }); - }; - trySend(); - }); -} diff --git a/dist/popup.js b/dist/popup.js deleted file mode 100644 index 0655776..0000000 --- a/dist/popup.js +++ /dev/null @@ -1,1400 +0,0 @@ -import { t, applyI18n } from "./i18n.js"; -import { encryptConfigs, decryptConfigs } from "./crypto-file.js"; -import { estimateStrength, formatTime } from "./strength.js"; -import { sendMessage } from "./messaging.js"; -import { openConfigUrl, fetchTitleByDomain, closeAllMenus } from "./ui-utils.js"; -document.addEventListener("DOMContentLoaded", () => { - const $ = (id) => document.getElementById(id); - applyI18n(); - const versionLabel = $("versionLabel"); - if (versionLabel) - versionLabel.textContent = "v" + chrome.runtime.getManifest().version; - const domainInput = $("domain"); - const aliasInput = $("alias"); - const usernameInput = $("username"); - const passwordInput = $("password"); - const autoSubmitSelect = $("autoSubmit"); - const enabledCheckbox = $("enabled"); - const delayMsInput = $("delayMs"); - const usernameSelectorInput = $("usernameSelector"); - const passwordSelectorInput = $("passwordSelector"); - const submitSelectorInput = $("submitSelector"); - const addBtn = $("addBtn"); - const configList = $("configList"); - const addSection = $("addSection"); - const listSection = $("listSection"); - const fillCurrentBtn = $("fillCurrent"); - const fillDomainBtn = $("fillDomain"); - const exportBtn = $("exportBtn"); - const importBtn = $("importBtn"); - const importFile = $("importFile"); - const importBookmarkBtn = $("importBookmarkBtn"); - const bookmarkModal = $("bookmarkModal"); - const bookmarkList = $("bookmarkList"); - const bookmarkSelectAll = $("bookmarkSelectAll"); - const bookmarkSearch = $("bookmarkSearch"); - const bookmarkCancel = $("bookmarkCancel"); - const bookmarkConfirm = $("bookmarkConfirm"); - const toast = $("toast"); - // 编辑配置弹窗 - const editConfigModal = $("editConfigModal"); - const editConfigModalTitle = $("editConfigModalTitle"); - const editDomainInput = $("editDomain"); - const editUsernameInput = $("editUsername"); - const editPasswordInput = $("editPassword"); - const editAliasInput = $("editAlias"); - const editAutoSubmitSelect = $("editAutoSubmit"); - const editEnabledCheckbox = $("editEnabled"); - const editDelayMsInput = $("editDelayMs"); - const editUsernameSelectorInput = $("editUsernameSelector"); - const editPasswordSelectorInput = $("editPasswordSelector"); - const editSubmitSelectorInput = $("editSubmitSelector"); - const editConfigCancel = $("editConfigCancel"); - const editConfigSave = $("editConfigSave"); - const editFillDomainBtn = $("editFillDomain"); - let editModalEditingId = null; - const addEntryBtn = $("addEntryBtn"); - const searchBtn = $("searchBtn"); - const searchBox = $("searchBox"); - const searchInput = $("searchInput"); - const searchClear = $("searchClear"); - const searchToggleBtn = $("searchToggleBtn"); - let searchKeyword = ""; - // 锁定界面 - const lockScreen = $("lockScreen"); - const mainContainer = $("mainContainer"); - const lockTitle = $("lockTitle"); - const lockHint = $("lockHint"); - const lockPassword = $("lockPassword"); - const lockPasswordConfirm = $("lockPasswordConfirm"); - const lockConfirmGroup = $("lockConfirmGroup"); - const lockSubmitBtn = $("lockSubmitBtn"); - const lockWarn = $("lockWarn"); - const lockBtn = $("lockBtn"); - // 环境栏 - const envSelect = $("envSelect"); - const envManageBtn = $("envManageBtn"); - const envManageModal = $("envManageModal"); - const envManageList = $("envManageList"); - const envManageWarn = $("envManageWarn"); - const envManageCloseX = $("envManageCloseX"); - const envManageAdd = $("envManageAdd"); - const envModal = $("envModal"); - const envModalTitle = $("envModalTitle"); - const envNameInput = $("envNameInput"); - const presetEnvTags = $("presetEnvTags"); - const envModalWarn = $("envModalWarn"); - const envModalCancel = $("envModalCancel"); - const envModalCloseX = $("envModalCloseX"); - const envModalConfirm = $("envModalConfirm"); - const pwdModal = $("pwdModal"); - const modalTitle = $("modalTitle"); - const encryptToggle = $("encryptToggle"); - const encryptToggleGroup = $("encryptToggleGroup"); - const pwdFields = $("pwdFields"); - const confirmGroup = $("confirmGroup"); - const modalPwd = $("modalPwd"); - const modalPwdConfirm = $("modalPwdConfirm"); - const modalWarn = $("modalWarn"); - const modalCancel = $("modalCancel"); - const modalConfirm = $("modalConfirm"); - const strengthBlock = $("strengthBlock"); - const strengthBar = $("strengthBar"); - const strengthLabel = $("strengthLabel"); - const crackTime = $("crackTime"); - let modalResolve = null; - // 密码强度估算:按 GPU 集群约 10^7 次/秒(PBKDF2-SHA256)计 - function updateStrength(pwd) { - const { entropy, seconds, level, label } = estimateStrength(pwd); - // 5 档进度条宽度,对数缩放避免弱口令条太短看不见 - const widthMap = { - "empty": 0, - "very-weak": 15, - "weak": 30, - "medium": 55, - "strong": 80, - "very-strong": 100, - }; - strengthBar.style.width = widthMap[level] + "%"; - strengthBar.className = "strength-bar-fg " + level; - strengthLabel.textContent = pwd ? t("strengthLabel", label, entropy.toFixed(1)) : t("strengthEmpty"); - crackTime.textContent = pwd ? t("crackTimeLabel", formatTime(seconds)) : ""; - } - modalPwd.addEventListener("input", () => { - if (strengthBlock.style.display !== "none") - updateStrength(modalPwd.value); - }); - // 文件密码 modal:export 显示加密勾选 + 两次输入,import 只输入一次 - function openPwdModal(mode) { - return new Promise((resolve) => { - modalResolve = resolve; - if (mode === "export") { - modalTitle.textContent = t("pwdModalExportTitle"); - encryptToggleGroup.style.display = ""; - confirmGroup.style.display = ""; - strengthBlock.style.display = ""; - encryptToggle.checked = true; - pwdFields.classList.remove("hidden"); - modalPwd.value = ""; - modalPwdConfirm.value = ""; - modalWarn.textContent = t("pwdModalExportWarn"); - } - else { - modalTitle.textContent = t("pwdModalImportTitle"); - encryptToggleGroup.style.display = "none"; - confirmGroup.style.display = "none"; - strengthBlock.style.display = "none"; - pwdFields.classList.remove("hidden"); - modalPwd.value = ""; - modalPwdConfirm.value = ""; - modalWarn.textContent = ""; - } - pwdModal.classList.add("show"); - setTimeout(() => modalPwd.focus(), 0); - }); - } - function closePwdModal() { - pwdModal.classList.remove("show"); - modalPwd.value = ""; - modalPwdConfirm.value = ""; - modalResolve = null; - updateStrength(""); - } - encryptToggle.addEventListener("change", () => { - if (encryptToggle.checked) { - pwdFields.classList.remove("hidden"); - updateStrength(modalPwd.value); - } - else { - pwdFields.classList.add("hidden"); - updateStrength(""); - } - }); - modalCancel.addEventListener("click", () => { - if (modalResolve) - modalResolve(null); - closePwdModal(); - }); - modalConfirm.addEventListener("click", () => { - if (!modalResolve) - return; - const isExport = modalTitle.textContent === t("pwdModalExportTitle"); - if (isExport) { - if (encryptToggle.checked) { - const p1 = modalPwd.value; - const p2 = modalPwdConfirm.value; - if (!p1) { - modalWarn.textContent = t("warnFilePwdEmpty"); - return; - } - if (p1 !== p2) { - modalWarn.textContent = t("warnFilePwdMismatch"); - return; - } - modalResolve({ encrypt: true, password: p1 }); - } - else { - modalResolve({ encrypt: false, password: null }); - } - } - else { - const p = modalPwd.value; - if (!p) { - modalWarn.textContent = t("warnFilePwdEmpty"); - return; - } - modalResolve({ encrypt: true, password: p }); - } - closePwdModal(); - }); - modalPwd.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - if (confirmGroup.style.display === "none") { - modalConfirm.click(); - } - else if (modalPwdConfirm.value === "") { - modalPwdConfirm.focus(); - } - else { - modalConfirm.click(); - } - } - }); - modalPwdConfirm.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - modalConfirm.click(); - } - }); - function showToast(msg) { - toast.textContent = msg; - toast.classList.add("show"); - setTimeout(() => toast.classList.remove("show"), 2000); - } - document.querySelectorAll(".toggle-pwd").forEach((btn) => { - btn.addEventListener("click", () => { - const target = $(btn.dataset.target || ""); - if (target.type === "password") { - target.type = "text"; - btn.classList.remove("eye-open"); - btn.classList.add("eye-closed"); - btn.textContent = "🙈"; - } - else { - target.type = "password"; - btn.classList.remove("eye-closed"); - btn.classList.add("eye-open"); - btn.textContent = "👁"; - } - }); - }); - // 表单内容实时存入草稿,弹窗关闭后不丢失 - const draftFields = [ - domainInput, aliasInput, usernameInput, passwordInput, autoSubmitSelect, - usernameSelectorInput, passwordSelectorInput, submitSelectorInput, - enabledCheckbox, delayMsInput, - ]; - draftFields.forEach((el) => { - el.addEventListener("input", saveDraft); - el.addEventListener("change", saveDraft); - }); - // 环境数据层(EnvStore) - const { loadEnvironments, saveEnvironments, setActiveEnv, createEnvironment, renameEnvironment, deleteEnvironment, loadConfigs, saveConfigs, loadAllConfigs, genId, genCfgId, } = EnvStore; - // isDomainMatch 由 utils.js 提供(全局函数) - const DRAFT_KEY = "formDraft"; - function saveDraft() { - const draft = { - domain: domainInput.value, - alias: aliasInput.value, - username: usernameInput.value, - password: passwordInput.value, - autoSubmit: autoSubmitSelect.value, - usernameSelector: usernameSelectorInput.value, - passwordSelector: passwordSelectorInput.value, - submitSelector: submitSelectorInput.value, - enabled: enabledCheckbox.checked, - delayMs: delayMsInput.value, - }; - chrome.storage.local.set({ [DRAFT_KEY]: draft }); - } - async function loadDraft() { - const data = await chrome.storage.local.get(DRAFT_KEY); - const draft = data[DRAFT_KEY]; - if (!draft) - return; - domainInput.value = draft.domain || ""; - aliasInput.value = draft.alias || ""; - usernameInput.value = draft.username || ""; - passwordInput.value = draft.password || ""; - autoSubmitSelect.value = draft.autoSubmit || "true"; - usernameSelectorInput.value = draft.usernameSelector || ""; - passwordSelectorInput.value = draft.passwordSelector || ""; - submitSelectorInput.value = draft.submitSelector || ""; - enabledCheckbox.checked = draft.enabled !== false; - delayMsInput.value = draft.delayMs || ""; - addBtn.textContent = t("addConfigBtn"); - } - function clearDraft() { - chrome.storage.local.remove(DRAFT_KEY); - } - // 环境栏 - let envModalResolve = null; - // mode: "create" | "rename" - function openEnvModal(mode, currentName = "") { - return new Promise((resolve) => { - envModalResolve = resolve; - if (mode === "create") { - envModalTitle.textContent = t("envCreateTitle"); - envNameInput.value = ""; - presetEnvTags.style.display = "flex"; - } - else { - envModalTitle.textContent = t("envRenameTitle"); - envNameInput.value = currentName; - presetEnvTags.style.display = "flex"; - } - envModalWarn.style.display = "none"; - envModalWarn.textContent = ""; - envModal.classList.add("show"); - setTimeout(() => { - envNameInput.focus(); - envNameInput.select(); - }, 0); - }); - } - function closeEnvModal() { - envModal.classList.remove("show"); - envNameInput.value = ""; - envModalWarn.style.display = "none"; - envModalResolve = null; - } - presetEnvTags.querySelectorAll(".preset-tag").forEach((tag) => { - tag.addEventListener("click", () => { - envNameInput.value = tag.dataset.name || ""; - envNameInput.focus(); - }); - }); - function closeEnvModalWithResolve() { - if (envModalResolve) - envModalResolve(null); - closeEnvModal(); - } - envModalCancel.addEventListener("click", closeEnvModalWithResolve); - envModalCloseX.addEventListener("click", closeEnvModalWithResolve); - envModalConfirm.addEventListener("click", async () => { - if (!envModalResolve) - return; - const name = envNameInput.value.trim(); - if (!name) { - envModalWarn.textContent = t("envWarnEmpty"); - envModalWarn.style.display = "block"; - return; - } - if (name.length > 20) { - envModalWarn.textContent = t("envWarnTooLong"); - envModalWarn.style.display = "block"; - return; - } - envModalResolve(name); - closeEnvModal(); - }); - envNameInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - envModalConfirm.click(); - } - }); - async function renderEnvSelect() { - const { environments, activeEnvId } = await loadEnvironments(); - envSelect.innerHTML = ""; - environments.forEach((env) => { - const opt = document.createElement("option"); - opt.value = env.id; - opt.textContent = `${env.name} (${env.configs?.length || 0})`; - if (env.id === activeEnvId) - opt.selected = true; - envSelect.appendChild(opt); - }); - } - envSelect.addEventListener("change", async () => { - await setActiveEnv(envSelect.value); - resetForm(); - await renderConfigList(); - showToast(t("toastSwitchEnv")); - }); - // 环境管理对话框 - function openEnvManageModal() { - envManageWarn.textContent = ""; - envManageWarn.style.display = "none"; - renderEnvManageList(); - envManageModal.classList.add("show"); - } - function closeEnvManageModal() { - envManageModal.classList.remove("show"); - } - async function renderEnvManageList() { - const { environments, activeEnvId } = await loadEnvironments(); - envManageList.innerHTML = ""; - if (environments.length === 0) { - envManageList.innerHTML = `
${t("envManageEmpty")}
`; - return; - } - environments.forEach((env) => { - const row = document.createElement("div"); - row.className = "env-manage-row" + (env.id === activeEnvId ? " active" : ""); - const nameSpan = document.createElement("span"); - nameSpan.className = "env-manage-name"; - const cnt = env.configs?.length || 0; - const activeMark = env.id === activeEnvId ? `${t("envCurrentMark")}` : ""; - nameSpan.innerHTML = `${escapeHtml(env.name)} (${cnt}) ${activeMark}`; - const actions = document.createElement("div"); - actions.className = "env-manage-actions"; - const editBtn = document.createElement("button"); - editBtn.className = "btn-icon"; - editBtn.title = t("renameBtn"); - editBtn.innerHTML = ''; - editBtn.addEventListener("click", async () => { - const newName = await openEnvModal("rename", env.name); - if (!newName) - return; - try { - await renameEnvironment(env.id, newName); - await renderEnvManageList(); - await renderEnvSelect(); - showToast(t("toastRenamed")); - } - catch (err) { - showToast(err.message || t("toastRenameFail")); - } - }); - const delBtn = document.createElement("button"); - delBtn.className = "btn-icon"; - delBtn.title = t("deleteBtn"); - delBtn.innerHTML = ''; - delBtn.addEventListener("click", async () => { - if (environments.length <= 1) { - showToast(t("toastKeepOneEnv")); - return; - } - const cnt2 = env.configs?.length || 0; - const msg = cnt2 > 0 - ? t("confirmDeleteEnvWithCfgs", env.name, String(cnt2)) - : t("confirmDeleteEnv", env.name); - if (!confirm(msg)) - return; - try { - await deleteEnvironment(env.id); - await renderEnvManageList(); - await renderEnvSelect(); - resetForm(); - await renderConfigList(); - showToast(t("toastEnvDeleted")); - } - catch (err) { - showToast(err.message || t("toastDeleteFail")); - } - }); - actions.appendChild(editBtn); - actions.appendChild(delBtn); - row.appendChild(nameSpan); - row.appendChild(actions); - envManageList.appendChild(row); - }); - } - envManageBtn.addEventListener("click", openEnvManageModal); - envManageCloseX.addEventListener("click", closeEnvManageModal); - envManageAdd.addEventListener("click", async () => { - const name = await openEnvModal("create"); - if (!name) - return; - try { - await createEnvironment(name); - await renderEnvManageList(); - await renderEnvSelect(); - resetForm(); - await renderConfigList(); - showToast(t("toastEnvCreated", name)); - } - catch (err) { - showToast(err.message || t("toastCreateFail")); - } - }); - document.addEventListener("click", (e) => { - if (!e.target.closest(".config-actions")) - closeAllMenus(); - }); - async function renderConfigList() { - const allConfigs = await loadConfigs(); - configList.innerHTML = ""; - // 无配置时禁用导出,此时表单在上、空状态的列表在下;有配置时反过来 - const isEmpty = allConfigs.length === 0; - addSection.classList.toggle("collapsed", !isEmpty); - exportBtn.disabled = isEmpty; - exportBtn.classList.toggle("btn-disabled", isEmpty); - const parent = addSection.parentElement; - const addIdx = Array.prototype.indexOf.call(parent.children, addSection); - const listIdx = Array.prototype.indexOf.call(parent.children, listSection); - if (isEmpty && addIdx > listIdx) { - parent.insertBefore(addSection, listSection); - } - else if (!isEmpty && listIdx > addIdx) { - parent.insertBefore(listSection, addSection); - } - if (isEmpty) { - configList.innerHTML = ` -
-
- - - - - - -
-
${t("emptyStateTitle")}
-
${t("emptyStateSub")}
-
- `; - return; - } - // 搜索过滤:支持域名、用户名、别名模糊检索 - const kw = searchKeyword.trim().toLowerCase(); - const configs = kw - ? allConfigs.filter((c) => (c.domain || "").toLowerCase().includes(kw) || - (c.username || "").toLowerCase().includes(kw) || - (c.alias || "").toLowerCase().includes(kw)) - : allConfigs; - if (configs.length === 0) { - configList.innerHTML = ` -
-
${t("noSearchResult")}
-
- `; - return; - } - let draggedConfigId = null; - configs.forEach((cfg) => { - const item = document.createElement("div"); - item.className = "config-item" + (cfg.enabled === false ? " config-disabled" : ""); - if (!kw) { - item.draggable = true; - item.addEventListener("dragstart", (e) => { - draggedConfigId = cfg.id; - item.classList.add("dragging"); - if (e.dataTransfer) - e.dataTransfer.effectAllowed = "move"; - }); - item.addEventListener("dragend", () => { - draggedConfigId = null; - item.classList.remove("dragging"); - configList.querySelectorAll(".config-item").forEach((el) => el.classList.remove("drag-over")); - }); - item.addEventListener("dragover", (e) => { - e.preventDefault(); - if (e.dataTransfer) - e.dataTransfer.dropEffect = "move"; - if (cfg.id !== draggedConfigId) { - item.classList.add("drag-over"); - } - }); - item.addEventListener("dragleave", () => { - item.classList.remove("drag-over"); - }); - item.addEventListener("drop", async (e) => { - e.preventDefault(); - item.classList.remove("drag-over"); - if (!draggedConfigId || draggedConfigId === cfg.id) - return; - const fromIdx = allConfigs.findIndex((c) => c.id === draggedConfigId); - const toIdx = allConfigs.findIndex((c) => c.id === cfg.id); - if (fromIdx === -1 || toIdx === -1) - return; - const [moved] = allConfigs.splice(fromIdx, 1); - allConfigs.splice(toIdx, 0, moved); - await saveConfigs(allConfigs); - renderConfigList(); - }); - } - const info = document.createElement("div"); - info.className = "config-info"; - const displayName = escapeHtml(cfg.alias || cfg.domain); - const enabledTag = cfg.enabled === false - ? `${t("tagDisabled")}` - : ""; - info.innerHTML = ` -
${displayName} ${enabledTag}
- `; - const actions = document.createElement("div"); - actions.className = "config-actions"; - const isDisabled = cfg.enabled === false; - // 编辑(图标按钮) - const editBtn = document.createElement("button"); - editBtn.className = "btn-icon"; - editBtn.title = t("editBtn"); - editBtn.innerHTML = ''; - editBtn.addEventListener("click", (e) => { - e.stopPropagation(); - editModalEditingId = cfg.id; - editConfigModalTitle.textContent = t("editConfigTitle"); - editConfigSave.textContent = t("updateConfigBtn"); - editDomainInput.value = cfg.domain; - editAliasInput.value = cfg.alias || ""; - editUsernameInput.value = cfg.username; - editPasswordInput.value = cfg.password; - editAutoSubmitSelect.value = String(cfg.autoSubmit); - editUsernameSelectorInput.value = cfg.usernameSelector || ""; - editPasswordSelectorInput.value = cfg.passwordSelector || ""; - editSubmitSelectorInput.value = cfg.submitSelector || ""; - editEnabledCheckbox.checked = cfg.enabled !== false; - editDelayMsInput.value = cfg.delayMs || ""; - editConfigModal.classList.add("show"); - editDomainInput.focus(); - }); - // 三个小点下拉菜单(收起禁用/删除) - const moreBtn = document.createElement("button"); - moreBtn.className = "btn-icon"; - moreBtn.title = t("moreActionsTitle"); - moreBtn.innerHTML = ''; - moreBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const open = menu.classList.contains("open"); - closeAllMenus(); - if (!open) - menu.classList.add("open"); - }); - const menu = document.createElement("div"); - menu.className = "config-menu"; - const toggleItem = document.createElement("button"); - toggleItem.className = "config-menu-item"; - toggleItem.textContent = isDisabled ? t("toggleEnable") : t("toggleDisable"); - toggleItem.addEventListener("click", async (e) => { - e.stopPropagation(); - closeAllMenus(); - const updated = configs.map((c) => c.id === cfg.id ? { ...c, enabled: c.enabled === false } : c); - await saveConfigs(updated); - renderConfigList(); - renderEnvSelect(); - if (envManageModal.classList.contains("show")) - renderEnvManageList(); - showToast(isDisabled ? t("toastEnabled") : t("toastDisabled")); - }); - const delItem = document.createElement("button"); - delItem.className = "config-menu-item danger"; - delItem.textContent = t("deleteBtn"); - delItem.addEventListener("click", async (e) => { - e.stopPropagation(); - closeAllMenus(); - const updated = configs.filter((c) => c.id !== cfg.id); - await saveConfigs(updated); - showToast(t("toastDeleted")); - renderConfigList(); - renderEnvSelect(); - if (envManageModal.classList.contains("show")) - renderEnvManageList(); - }); - menu.appendChild(toggleItem); - menu.appendChild(delItem); - actions.appendChild(editBtn); - actions.appendChild(moreBtn); - actions.appendChild(menu); - // 点击整项 → 新窗口打开 - item.addEventListener("click", () => openConfigUrl(cfg.domain)); - item.appendChild(info); - item.appendChild(actions); - configList.appendChild(item); - }); - } - function resetForm() { - domainInput.value = ""; - aliasInput.value = ""; - usernameInput.value = ""; - passwordInput.value = ""; - autoSubmitSelect.value = "true"; - usernameSelectorInput.value = ""; - passwordSelectorInput.value = ""; - submitSelectorInput.value = ""; - enabledCheckbox.checked = true; - delayMsInput.value = ""; - addBtn.textContent = t("addConfigBtn"); - clearDraft(); - } - addBtn.addEventListener("click", async () => { - const domain = domainInput.value.trim(); - const username = usernameInput.value.trim(); - const password = passwordInput.value; - if (!domain || !username || !password) { - showToast(t("toastRequired")); - return; - } - const config = { - id: genCfgId(), - domain, - alias: aliasInput.value.trim() || await fetchTitleByDomain(domain) || null, - username, - password, - autoSubmit: autoSubmitSelect.value === "true", - usernameSelector: usernameSelectorInput.value.trim() || null, - passwordSelector: passwordSelectorInput.value.trim() || null, - submitSelector: submitSelectorInput.value.trim() || null, - enabled: enabledCheckbox.checked, - delayMs: delayMsInput.value ? Number(delayMsInput.value) : null, - }; - const configs = await loadConfigs(); - configs.push(config); - showToast(t("toastAdded")); - await saveConfigs(configs); - resetForm(); - renderConfigList(); - renderEnvSelect(); - if (envManageModal.classList.contains("show")) - renderEnvManageList(); - }); - // 编辑配置弹窗:保存 - editConfigSave.addEventListener("click", async () => { - const domain = editDomainInput.value.trim(); - const username = editUsernameInput.value.trim(); - const password = editPasswordInput.value; - if (!domain || !username || !password) { - showToast(t("toastRequired")); - return; - } - const config = { - id: editModalEditingId || genCfgId(), - domain, - alias: editAliasInput.value.trim() || await fetchTitleByDomain(domain) || null, - username, - password, - autoSubmit: editAutoSubmitSelect.value === "true", - usernameSelector: editUsernameSelectorInput.value.trim() || null, - passwordSelector: editPasswordSelectorInput.value.trim() || null, - submitSelector: editSubmitSelectorInput.value.trim() || null, - enabled: editEnabledCheckbox.checked, - delayMs: editDelayMsInput.value ? Number(editDelayMsInput.value) : null, - }; - const configs = await loadConfigs(); - if (editModalEditingId) { - const idx = configs.findIndex((c) => c.id === editModalEditingId); - if (idx !== -1) { - configs[idx] = config; - } - showToast(t("toastUpdated")); - } - else { - configs.push(config); - showToast(t("toastAdded")); - } - await saveConfigs(configs); - editConfigModal.classList.remove("show"); - editModalEditingId = null; - renderConfigList(); - renderEnvSelect(); - if (envManageModal.classList.contains("show")) - renderEnvManageList(); - }); - // 添加配置入口按钮:打开新增弹窗 - addEntryBtn.addEventListener("click", () => { - editModalEditingId = null; - editConfigModalTitle.textContent = t("addConfigTitle"); - editConfigSave.textContent = t("okBtn"); - editDomainInput.value = ""; - editUsernameInput.value = ""; - editPasswordInput.value = ""; - editAliasInput.value = ""; - editAutoSubmitSelect.value = "true"; - editEnabledCheckbox.checked = true; - editDelayMsInput.value = ""; - editUsernameSelectorInput.value = ""; - editPasswordSelectorInput.value = ""; - editSubmitSelectorInput.value = ""; - editConfigModal.classList.add("show"); - editDomainInput.focus(); - }); - // 搜索按钮:展开内联搜索框 - searchBtn.addEventListener("click", () => { - searchBtn.hidden = true; - searchBox.hidden = false; - searchInput.focus(); - }); - // 搜索框内搜索图标:收起回图标按钮 - searchToggleBtn.addEventListener("click", () => { - searchInput.value = ""; - searchKeyword = ""; - searchClear.hidden = true; - searchBox.hidden = true; - searchBtn.hidden = false; - renderConfigList(); - }); - // 搜索输入:实时过滤 - searchInput.addEventListener("input", () => { - searchKeyword = searchInput.value; - searchClear.hidden = !searchKeyword; - renderConfigList(); - }); - // 清除搜索 - searchClear.addEventListener("click", () => { - searchInput.value = ""; - searchKeyword = ""; - searchClear.hidden = true; - renderConfigList(); - searchInput.focus(); - }); - // 编辑配置弹窗:取消 - const editConfigCloseX = $("editConfigCloseX"); - function closeEditConfigModal() { - editConfigModal.classList.remove("show"); - editModalEditingId = null; - } - editConfigCancel.addEventListener("click", closeEditConfigModal); - editConfigCloseX.addEventListener("click", closeEditConfigModal); - // 点击遮罩关闭编辑弹窗 - editConfigModal.addEventListener("click", (e) => { - if (e.target === editConfigModal) { - editConfigModal.classList.remove("show"); - editModalEditingId = null; - } - }); - fillCurrentBtn.addEventListener("click", async () => { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - if (!tab || tab.id === undefined) { - showToast(t("toastNoTab")); - return; - } - // 跨所有环境查找匹配当前标签页的配置 - const allConfigs = await loadAllConfigs(); - const matched = allConfigs.filter((c) => isDomainMatch(tab.url, c.domain)); - if (matched.length === 0) { - showToast(t("toastNoMatch")); - return; - } - if (matched.length === 1) { - try { - const resp = await chrome.tabs.sendMessage(tab.id, { - action: "manualFill", - config: matched[0], - }); - if (resp && resp.success) { - showToast(t("toastFilled")); - } - else if (resp && resp.diag) { - const d = resp.diag; - showToast(`input=${d.total} pwd=${d.pwd} form=${d.forms} | ${d.sample.join(", ")}`); - } - else { - showToast(t("toastFilledNoForm")); - } - } - catch (err) { - // content script 未注入:常见于安装扩展前已打开的页面 - if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) { - showToast(t("toastScriptNotInjected")); - } - else { - showToast(t("toastSendFail", err.message || String(err))); - } - } - return; - } - try { - await chrome.tabs.sendMessage(tab.id, { - action: "manualFillMulti", - configs: matched, - tabUrl: tab.url, - }); - showToast(t("toastMultiMatch", String(matched.length))); - } - catch (err) { - if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) { - showToast(t("toastScriptNotInjected")); - } - else { - showToast(t("toastSendFail", err.message || String(err))); - } - } - }); - fillDomainBtn.addEventListener("click", async () => { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - if (!tab || !tab.url) { - showToast(t("toastNoTab")); - return; - } - try { - const u = new URL(tab.url); - const host = u.host; - if (!host) { - showToast(t("toastNoDomain")); - return; - } - const value = u.pathname && u.pathname !== "/" ? host + u.pathname : host; - domainInput.value = value; - if (tab.title) - aliasInput.value = tab.title; - saveDraft(); - showToast(t("toastDomainFilled", value)); - } - catch { - showToast(t("toastUrlParseFail")); - } - }); - editFillDomainBtn.addEventListener("click", async () => { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - if (!tab || !tab.url) { - showToast(t("toastNoTab")); - return; - } - try { - const hostname = new URL(tab.url).hostname; - if (!hostname) { - showToast(t("toastNoDomain")); - return; - } - editDomainInput.value = hostname; - showToast(t("toastDomainFilled", hostname)); - } - catch { - showToast(t("toastUrlParseFail")); - } - }); - // 导出整个 environments 结构(含环境名) - exportBtn.addEventListener("click", async () => { - const { environments } = await loadEnvironments(); - const totalConfigs = environments.reduce((s, e) => s + (e.configs?.length || 0), 0); - if (totalConfigs === 0) { - showToast(t("toastNoExportable")); - return; - } - const choice = await openPwdModal("export"); - if (!choice) { - showToast(t("toastExportCancelled")); - return; - } - const stamp = new Date().toISOString().replace(/[:T]/g, "").slice(0, 15); - let payload; - let filename; - if (choice.encrypt) { - const cipher = await encryptConfigs(environments, choice.password); - payload = { - app: "AutoLoginManager", - version: 3, - exportedAt: new Date().toISOString(), - encrypted: true, - cipher, - }; - filename = `auto-login-configs-${stamp}.enc.json`; - } - else { - payload = { - app: "AutoLoginManager", - version: 3, - exportedAt: new Date().toISOString(), - encrypted: false, - environments, - }; - filename = `auto-login-configs-${stamp}.json`; - } - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - showToast(t("toastExported", String(environments.length), String(totalConfigs), choice.encrypt ? t("encryptedMark") : "")); - }); - // 导入配置:兼容 v1(数组) / v2(configs) / v3(environments),加密则弹口令 modal - importBtn.addEventListener("click", () => importFile.click()); - importFile.addEventListener("change", async (e) => { - const file = e.target.files?.[0]; - if (!file) - return; - try { - const text = await file.text(); - const data = JSON.parse(text); - let incomingEnvironments = null; - let incomingConfigs = null; - // 1. v1: 直接数组格式 - if (Array.isArray(data)) { - incomingConfigs = data; - } - // 2. 加密格式 - else if (data?.encrypted === true && data?.cipher) { - const choice = await openPwdModal("import"); - if (!choice) { - showToast(t("toastImportCancelled")); - importFile.value = ""; - return; - } - try { - const decrypted = await decryptConfigs(data.cipher, choice.password); - // 解密后可能是 v3 (environments 数组/对象) 或 v1 (configs 数组) - if (Array.isArray(decrypted)) { - // 区分环境数组(v3, 元素含 configs)与配置数组(v1, 元素含 domain) - if (decrypted.some((e) => e && Array.isArray(e.configs))) { - incomingEnvironments = decrypted; - } - else { - incomingConfigs = decrypted; - } - } - else if (Array.isArray(decrypted?.environments)) { - incomingEnvironments = decrypted.environments; - } - else if (Array.isArray(decrypted?.configs)) { - incomingConfigs = decrypted.configs; - } - else { - showToast(t("toastInvalidFormat")); - importFile.value = ""; - return; - } - } - catch (err) { - showToast(t("toastDecryptFail")); - importFile.value = ""; - return; - } - } - // 3. v3 明文:含 environments - else if (Array.isArray(data?.environments)) { - incomingEnvironments = data.environments; - } - // 4. v2 明文:含 configs - else if (Array.isArray(data?.configs)) { - incomingConfigs = data.configs; - } - else { - showToast(t("toastFileInvalid")); - importFile.value = ""; - return; - } - const { environments: existingEnvs, activeEnvId } = await loadEnvironments(); - // 分支 A:导入 v3 环境结构 —— 按"环境名"匹配,已存在的环境跳过,新的环境整批导入 - if (incomingEnvironments) { - const existingNames = new Set(existingEnvs.map((e) => e.name)); - let addedEnvs = 0; - let addedCfgs = 0; - let skippedCfgs = 0; - for (const env of incomingEnvironments) { - if (!env || !env.name) { - skippedCfgs += (env?.configs?.length) || 0; - continue; - } - if (existingNames.has(env.name)) { - // 同名环境:跳过整个环境 - skippedCfgs += (env.configs?.length) || 0; - continue; - } - existingNames.add(env.name); - const newEnv = { - id: genId(), - name: env.name, - configs: [], - }; - for (const cfg of (env.configs || [])) { - if (!cfg || !cfg.domain || !cfg.username || !cfg.password) { - skippedCfgs++; - continue; - } - newEnv.configs.push({ - id: cfg.id || genCfgId(), - domain: cfg.domain, - alias: cfg.alias || null, - username: cfg.username, - password: cfg.password, - autoSubmit: cfg.autoSubmit !== false, - usernameSelector: cfg.usernameSelector || null, - passwordSelector: cfg.passwordSelector || null, - submitSelector: cfg.submitSelector || null, - enabled: cfg.enabled !== false, - delayMs: cfg.delayMs || null, - }); - addedCfgs++; - } - existingEnvs.push(newEnv); - addedEnvs++; - } - await saveEnvironments(existingEnvs, activeEnvId ?? undefined); - await renderEnvSelect(); - await renderConfigList(); - showToast(t("toastImportedEnvs", String(addedEnvs), String(addedCfgs), String(skippedCfgs))); - return; - } - // 分支 B:导入 v1/v2 配置数组 —— 合并到当前激活环境,相同 domain 跳过 - const curEnv = existingEnvs.find((e) => e.id === activeEnvId) || existingEnvs[0]; - const seenDomains = new Set(curEnv.configs.map((c) => c.domain)); - let added = 0; - let skipped = 0; - for (const cfg of incomingConfigs || []) { - if (!cfg || !cfg.domain || !cfg.username || !cfg.password) { - skipped++; - continue; - } - if (seenDomains.has(cfg.domain)) { - skipped++; - continue; - } - seenDomains.add(cfg.domain); - curEnv.configs.push({ - id: cfg.id || genCfgId(), - domain: cfg.domain, - alias: cfg.alias || null, - username: cfg.username, - password: cfg.password, - autoSubmit: cfg.autoSubmit !== false, - usernameSelector: cfg.usernameSelector || null, - passwordSelector: cfg.passwordSelector || null, - submitSelector: cfg.submitSelector || null, - enabled: cfg.enabled !== false, - delayMs: cfg.delayMs || null, - }); - added++; - } - await saveEnvironments(existingEnvs); - await renderConfigList(); - showToast(t("toastImportedCfgs", String(added), String(skipped))); - } - catch (err) { - showToast(t("toastImportFail", err.message || "parse error")); - } - finally { - importFile.value = ""; - } - }); - // 从书签导入 - function flattenBookmarks(nodes, out) { - for (const node of nodes) { - if (node.url) { - out.push({ title: node.title || node.url, url: node.url }); - } - if (node.children) { - flattenBookmarks(node.children, out); - } - } - } - function urlToDomain(url) { - try { - const u = new URL(url); - let domain = u.host + u.pathname; - if (domain.endsWith("/") && u.pathname !== "/") - domain = domain.replace(/\/$/, ""); - if (u.pathname === "/") - domain = u.host; - return domain; - } - catch (e) { - return null; - } - } - let bookmarkItems = []; - function renderBookmarkList(filter) { - const kw = (filter || "").trim().toLowerCase(); - bookmarkList.innerHTML = ""; - const items = kw - ? bookmarkItems.filter((b) => b.title.toLowerCase().includes(kw) || b.url.toLowerCase().includes(kw)) - : bookmarkItems; - const frag = document.createDocumentFragment(); - for (const b of items) { - const row = document.createElement("label"); - row.className = "bookmark-item"; - const cb = document.createElement("input"); - cb.type = "checkbox"; - cb.dataset.url = b.url; - const text = document.createElement("span"); - text.className = "bookmark-item-text"; - text.textContent = b.title; - const sub = document.createElement("span"); - sub.className = "bookmark-item-url"; - sub.textContent = b.url; - row.appendChild(cb); - row.appendChild(text); - row.appendChild(sub); - frag.appendChild(row); - } - bookmarkList.appendChild(frag); - updateBookmarkSelectAllState(); - } - function updateBookmarkSelectAllState() { - const cbs = bookmarkList.querySelectorAll("input[type=checkbox]"); - const checked = bookmarkList.querySelectorAll("input[type=checkbox]:checked"); - bookmarkSelectAll.checked = cbs.length > 0 && cbs.length === checked.length; - bookmarkConfirm.disabled = checked.length === 0; - } - importBookmarkBtn.addEventListener("click", async () => { - try { - const tree = await chrome.bookmarks.getTree(); - bookmarkItems = []; - flattenBookmarks(tree, bookmarkItems); - bookmarkSearch.value = ""; - bookmarkSelectAll.checked = false; - renderBookmarkList(""); - bookmarkModal.classList.add("show"); - } - catch (e) { - showToast(t("bookmarkLoadFail")); - } - }); - bookmarkSearch.addEventListener("input", () => { - renderBookmarkList(bookmarkSearch.value); - }); - bookmarkList.addEventListener("change", () => { - updateBookmarkSelectAllState(); - }); - bookmarkSelectAll.addEventListener("change", () => { - const cbs = bookmarkList.querySelectorAll("input[type=checkbox]"); - cbs.forEach((cb) => (cb.checked = bookmarkSelectAll.checked)); - bookmarkConfirm.disabled = !bookmarkSelectAll.checked; - }); - bookmarkCancel.addEventListener("click", () => { - bookmarkModal.classList.remove("show"); - }); - bookmarkModal.addEventListener("click", (e) => { - if (e.target === bookmarkModal) - bookmarkModal.classList.remove("show"); - }); - bookmarkConfirm.addEventListener("click", async () => { - const checked = bookmarkList.querySelectorAll("input[type=checkbox]:checked"); - if (!checked.length) - return; - const configs = await loadConfigs(); - const existingDomains = new Set(configs.map((c) => c.domain)); - let added = 0; - let skipped = 0; - for (const cb of checked) { - const domain = urlToDomain(cb.dataset.url); - if (!domain || existingDomains.has(domain)) { - skipped++; - continue; - } - existingDomains.add(domain); - configs.push({ - id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8), - domain, - alias: cb.parentElement.querySelector(".bookmark-item-text").textContent || null, - username: "", - password: "", - autoSubmit: true, - usernameSelector: null, - passwordSelector: null, - submitSelector: null, - enabled: true, - delayMs: null, - }); - added++; - } - await saveConfigs(configs); - bookmarkModal.classList.remove("show"); - renderConfigList(); - renderEnvSelect(); - showToast(t("toastBookmarkImported", String(added), String(skipped))); - }); - // 锁定 / 解锁 - let lockMode = "setup"; // "setup" | "unlock" - let lockAfterSetup = false; - function showLockScreen(mode) { - lockMode = mode; - lockScreen.style.display = "flex"; - mainContainer.style.display = "none"; - lockPassword.value = ""; - lockPasswordConfirm.value = ""; - lockWarn.textContent = ""; - lockWarn.style.display = "none"; - if (mode === "setup") { - lockTitle.textContent = t("lockSetupTitle"); - lockHint.textContent = t("lockSetupHint"); - lockConfirmGroup.style.display = "block"; - lockSubmitBtn.textContent = t("lockSetupBtn"); - } - else { - lockTitle.textContent = t("lockUnlockTitle"); - lockHint.textContent = t("lockUnlockHint"); - lockConfirmGroup.style.display = "none"; - lockSubmitBtn.textContent = t("lockUnlockBtn"); - } - setTimeout(() => lockPassword.focus(), 50); - } - function showMainUI() { - lockScreen.style.display = "none"; - mainContainer.style.display = "flex"; - } - async function initLockState() { - try { - const resp = await sendMessage({ action: "getVaultStatus" }); - lockBtn.hidden = false; - if (!resp.hasVault) { - // 无 vault:明文模式,直接进入,不锁定 - showMainUI(); - loadDraft(); - renderEnvSelect(); - renderConfigList(); - } - else if (!resp.unlocked) { - showLockScreen("unlock"); - } - else { - showMainUI(); - loadDraft(); - renderEnvSelect(); - renderConfigList(); - } - } - catch (err) { - console.warn("[AutoLogin]", t("logGetLockStatusFail"), err); - lockBtn.hidden = false; - showMainUI(); - loadDraft(); - renderEnvSelect(); - renderConfigList(); - } - } - lockSubmitBtn.addEventListener("click", async () => { - const pwd = lockPassword.value; - if (!pwd) { - lockWarn.textContent = t("lockWarnEmpty"); - lockWarn.style.display = "block"; - return; - } - if (lockMode === "setup") { - if (pwd.length < 6) { - lockWarn.textContent = t("lockWarnShort"); - lockWarn.style.display = "block"; - return; - } - if (pwd !== lockPasswordConfirm.value) { - lockWarn.textContent = t("lockWarnMismatch"); - lockWarn.style.display = "block"; - return; - } - try { - await sendMessage({ action: "setupVault", password: pwd }); - showToast(t("toastPasswordSet")); - if (lockAfterSetup) { - lockAfterSetup = false; - await sendMessage({ action: "lock" }); - showLockScreen("unlock"); - showToast(t("toastLocked")); - } - else { - showMainUI(); - loadDraft(); - renderEnvSelect(); - renderConfigList(); - } - } - catch (err) { - lockWarn.textContent = t("lockWarnSetupFail", err.message || String(err)); - lockWarn.style.display = "block"; - } - } - else { - try { - const resp = await sendMessage({ action: "unlock", password: pwd }); - if (resp.success) { - showToast(t("toastUnlocked")); - showMainUI(); - loadDraft(); - renderEnvSelect(); - renderConfigList(); - } - else { - lockWarn.textContent = resp.error || t("lockWarnUnlockFail", ""); - lockWarn.style.display = "block"; - } - } - catch (err) { - lockWarn.textContent = t("lockWarnUnlockFail", err.message || String(err)); - lockWarn.style.display = "block"; - } - } - }); - lockPassword.addEventListener("keydown", (e) => { - if (e.key === "Enter") - lockSubmitBtn.click(); - }); - lockPasswordConfirm.addEventListener("keydown", (e) => { - if (e.key === "Enter") - lockSubmitBtn.click(); - }); - lockBtn.addEventListener("click", async () => { - const resp = await sendMessage({ action: "getVaultStatus" }).catch(() => null); - if (resp && !resp.hasVault) { - // 明文模式:先设密码再锁定 - lockAfterSetup = true; - showLockScreen("setup"); - return; - } - if (!confirm(t("lockConfirmPrompt"))) - return; - await sendMessage({ action: "lock" }); - showLockScreen("unlock"); - showToast(t("toastLocked")); - }); - // 弹窗内的点击视为用户活动,重置 background 自动锁定计时 - document.addEventListener("click", () => { - if (lockScreen.style.display === "none") { - sendMessage({ action: "ping" }, 1).catch(() => { }); - } - }); - initLockState(); -}); diff --git a/dist/strength.js b/dist/strength.js deleted file mode 100644 index 9ec26fc..0000000 --- a/dist/strength.js +++ /dev/null @@ -1,67 +0,0 @@ -import { t } from "./i18n.js"; -const ATTACK_RATE = 1e7; -export function estimateStrength(pwd) { - if (!pwd) - return { entropy: 0, seconds: 0, level: "empty", label: t("strengthEmpty") }; - let pool = 0; - if (/[a-z]/.test(pwd)) - pool += 26; - if (/[A-Z]/.test(pwd)) - pool += 26; - if (/[0-9]/.test(pwd)) - pool += 10; - if (/[^a-zA-Z0-9]/.test(pwd)) - pool += 32; - if (pool === 0) - pool = 1; - const entropy = pwd.length * Math.log2(pool); - const combinations = Math.pow(2, entropy); - const seconds = combinations / 2 / ATTACK_RATE; - let level; - let label; - if (seconds < 1) { - level = "very-weak"; - label = t("strengthVeryWeak"); - } - else if (seconds < 3600) { - level = "weak"; - label = t("strengthWeak"); - } - else if (seconds < 86400 * 30) { - level = "medium"; - label = t("strengthMedium"); - } - else if (seconds < 86400 * 365 * 1000) { - level = "strong"; - label = t("strengthStrong"); - } - else { - level = "very-strong"; - label = t("strengthVeryStrong"); - } - return { entropy, seconds, level, label }; -} -export function formatTime(seconds) { - if (seconds < 1) - return t("crackInstant"); - if (seconds < 60) - return t("crackSeconds", String(Math.round(seconds))); - if (seconds < 3600) - return t("crackMinutes", String(Math.round(seconds / 60))); - if (seconds < 86400) - return t("crackHours", String(Math.round(seconds / 3600))); - if (seconds < 86400 * 30) - return t("crackDays", String(Math.round(seconds / 86400))); - if (seconds < 86400 * 365) - return t("crackMonths", String(Math.round(seconds / 86400 / 30))); - const years = seconds / 86400 / 365; - if (years < 1000) - return t("crackYears", String(Math.round(years))); - if (years < 1e6) - return t("crackThousandYears", String(Math.round(years / 1000))); - if (years < 1e9) - return t("crackMillionYears", String(Math.round(years / 1e6))); - if (years < 1e12) - return t("crackBillionYears", String(Math.round(years / 1e9))); - return t("crackTrillionYears", (years / 1e12).toExponential(2)); -} diff --git a/dist/ui-utils.js b/dist/ui-utils.js deleted file mode 100644 index c8388ac..0000000 --- a/dist/ui-utils.js +++ /dev/null @@ -1,24 +0,0 @@ -export function openConfigUrl(domain) { - if (!domain) - return; - const url = /^https?:\/\//i.test(domain) ? domain : "http://" + domain; - window.open(url, "_blank", "noopener"); -} -export async function fetchTitleByDomain(domain) { - try { - const base = /^https?:\/\//i.test(domain) ? domain : "*://" + domain; - const patterns = [base]; - if (!base.endsWith("*")) - patterns.push(base + "/*"); - const tabs = await chrome.tabs.query({ url: patterns }); - for (const tab of tabs) { - if (tab.title) - return tab.title; - } - } - catch (e) { } - return null; -} -export function closeAllMenus() { - document.querySelectorAll(".config-menu.open").forEach((m) => m.classList.remove("open")); -} diff --git a/dist/utils.js b/dist/utils.js deleted file mode 100644 index 3230b39..0000000 --- a/dist/utils.js +++ /dev/null @@ -1,80 +0,0 @@ -"use strict"; -// URL 匹配 + HTML 转义,浏览器与 Node 通用 -(function (root, factory) { - if (typeof module === "object" && module.exports) { - module.exports = factory(); - } - else { - const api = factory(); - root.isDomainMatch = api.isDomainMatch; - root.escapeHtml = api.escapeHtml; - } -})(typeof globalThis !== "undefined" ? globalThis : this, function () { - "use strict"; - /** - * 判断 URL 是否匹配规则。 - * 纯 hostname:example.com / *.example.com - * hostname+path:example.com/login/*,路径部分支持 * 通配 - */ - function isDomainMatch(url, pattern) { - if (!url || !pattern) - return false; - try { - const u = new URL(url); - const rawPattern = String(pattern).trim().replace(/^https?:\/\//i, ""); - if (rawPattern.includes("/")) { - const slashIdx = rawPattern.indexOf("/"); - const hostPart = rawPattern.slice(0, slashIdx); - const pathPart = rawPattern.slice(slashIdx + 1) || ""; - const hostForMatch = hostPart.includes(":") ? u.host : u.hostname; - if (!matchHost(hostForMatch, hostPart)) - return false; - return matchPath(u.pathname, pathPart); - } - const hostForMatch = rawPattern.includes(":") ? u.host : u.hostname; - return matchHost(hostForMatch, rawPattern); - } - catch { - return false; - } - } - function matchHost(hostname, hostPart) { - if (hostPart.startsWith("*.")) { - const suffix = hostPart.slice(2); - return hostname === suffix || hostname.endsWith("." + suffix); - } - if (hostPart.includes("*")) { - const regexStr = hostPart.replace(/\./g, "\\.").replace(/\*/g, ".*"); - return new RegExp("^" + regexStr + "$").test(hostname); - } - return hostname === hostPart; - } - function matchPath(pathname, pathPart) { - const pagePath = pathname.startsWith("/") ? pathname.slice(1) : pathname; - if (!pathPart) - return true; - if (pathPart === "*") - return true; - if (pathPart.endsWith("/*")) { - const prefix = pathPart.slice(0, -2); - return pagePath === prefix || pagePath.startsWith(prefix + "/"); - } - if (pathPart.includes("*")) { - const regexStr = pathPart.replace(/\//g, "\\/").replace(/\*/g, ".*"); - return new RegExp("^" + regexStr + "$").test(pagePath); - } - return pagePath === pathPart; - } - /** HTML 转义,防止配置中的特殊字符破坏页面 */ - function escapeHtml(str) { - if (str == null) - return ""; - return String(str) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - return { isDomainMatch, escapeHtml }; -});