document.addEventListener("DOMContentLoaded", () => { const $ = (id) => document.getElementById(id); // i18n const t = (key, ...args) => { const msg = chrome.i18n.getMessage(key, args); return msg || key; }; // 遍历 data-i18n* 属性应用翻译;含嵌套标签的元素只替换首个文本节点 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); }); } 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 addConfigTitleEl = $("addConfigTitle"); const cancelEditBtn = $("cancelEditBtn"); 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 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 envManageClose = $("envManageClose"); const envManageAdd = $("envManageAdd"); const envModal = $("envModal"); const envModalTitle = $("envModalTitle"); const envNameInput = $("envNameInput"); const presetEnvTags = $("presetEnvTags"); const envModalWarn = $("envModalWarn"); const envModalCancel = $("envModalCancel"); 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 editingId = null; let modalResolve = null; // 密码强度估算:按 GPU 集群约 10^7 次/秒(PBKDF2-SHA256)计 const ATTACK_RATE = 1e7; 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, 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 }; } 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)); } 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); }); // Web Crypto 工具:AES-GCM + PBKDF2 const enc = new TextEncoder(); const dec = new TextDecoder(); 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); } 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"] ); } 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) } }; } 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)); } // 文件密码 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, getActiveEnv, 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, editingId: editingId, }; 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 || ""; editingId = null; // 编辑已改为弹窗,顶部表单仅用于新增;忽略历史 draft 中的 editingId addBtn.textContent = t("addConfigBtn"); addConfigTitleEl.textContent = t("addConfigTitle"); cancelEditBtn.hidden = true; } 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(); }); }); envModalCancel.addEventListener("click", () => { if (envModalResolve) envModalResolve(null); closeEnvModal(); }); 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-edit"; editBtn.textContent = t("renameBtn"); 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-danger"; delBtn.textContent = t("deleteBtn"); 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); envManageClose.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")); } }); 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; } configs.forEach((cfg) => { const item = document.createElement("div"); item.className = "config-item" + (cfg.enabled === false ? " config-disabled" : ""); const info = document.createElement("div"); info.className = "config-info"; const displayName = escapeHtml(cfg.alias || cfg.domain); const showDomain = cfg.alias ? escapeHtml(cfg.domain) : ""; const enabledTag = cfg.enabled === false ? `${t("tagDisabled")}` : ""; const delayTag = cfg.delayMs && Number(cfg.delayMs) > 300 ? `${t("tagDelay", String(cfg.delayMs))}` : ""; const autoSubmitText = cfg.autoSubmit ? t("yesShort") : t("noShort"); info.innerHTML = `
${displayName} ${enabledTag} ${delayTag}
${showDomain ? `
${showDomain}
` : ""}
${t("configDetailUser", escapeHtml(cfg.username), autoSubmitText)}
`; const actions = document.createElement("div"); actions.className = "config-actions"; // 启用/禁用切换 const toggleBtn = document.createElement("button"); toggleBtn.className = "btn-toggle"; const isDisabled = cfg.enabled === false; toggleBtn.textContent = isDisabled ? t("toggleEnable") : t("toggleDisable"); toggleBtn.title = isDisabled ? t("toggleEnableTitle") : t("toggleDisableTitle"); toggleBtn.addEventListener("click", async () => { 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 editBtn = document.createElement("button"); editBtn.className = "btn-edit"; editBtn.textContent = t("editBtn"); editBtn.addEventListener("click", () => { 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 delBtn = document.createElement("button"); delBtn.className = "btn-danger"; delBtn.textContent = t("deleteBtn"); delBtn.addEventListener("click", async () => { const updated = configs.filter((c) => c.id !== cfg.id); await saveConfigs(updated); showToast(t("toastDeleted")); renderConfigList(); renderEnvSelect(); if (envManageModal.classList.contains("show")) renderEnvManageList(); if (editingId === cfg.id) { resetForm(); } }); actions.appendChild(toggleBtn); actions.appendChild(editBtn); actions.appendChild(delBtn); item.appendChild(info); item.appendChild(actions); configList.appendChild(item); }); } function resetForm() { editingId = null; 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"); addConfigTitleEl.textContent = t("addConfigTitle"); cancelEditBtn.hidden = true; clearDraft(); } cancelEditBtn.addEventListener("click", () => { resetForm(); }); 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 = { domain, alias: aliasInput.value.trim() || 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(); if (editingId) { const idx = configs.findIndex((c) => c.id === editingId); if (idx !== -1) { config.id = editingId; configs[idx] = config; } showToast(t("toastUpdated")); } else { config.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8); 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 = { domain, alias: editAliasInput.value.trim() || 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) { config.id = editModalEditingId; configs[idx] = config; } showToast(t("toastUpdated")); } else { config.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8); 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(); }); // 编辑配置弹窗:取消 editConfigCancel.addEventListener("click", () => { editConfigModal.classList.remove("show"); editModalEditingId = null; }); // 点击遮罩关闭编辑弹窗 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) { 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); 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 = ""; } }); // 消息发送:SW 刚休眠唤醒时首次 sendMessage 会失败,需重试 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) { // 接收端不存在:SW 还没注册 listener,等 200ms 重试 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(); }); } // 锁定 / 解锁 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(); });