Files
auto-login/popup.js
T
2026-09-14 23:07:15 +08:00

1314 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 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");
let editModalEditingId = null;
// 锁定界面
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", () => 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 = "";
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";
pwdFields.classList.remove("hidden");
modalPwd.value = "";
modalPwdConfirm.value = "";
modalWarn.textContent = t("pwdModalImportWarn");
}
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 = `<div class="env-manage-empty">${t("envManageEmpty")}</div>`;
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 ? `<span class="env-manage-active-mark">${t("envCurrentMark")}</span>` : "";
nameSpan.innerHTML = `${escapeHtml(env.name)} <span class="env-manage-count">(${cnt})</span> ${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 configs = await loadConfigs();
configList.innerHTML = "";
// 无配置时禁用导出,此时表单在上、空状态的列表在下;有配置时反过来
const isEmpty = configs.length === 0;
exportBtn.disabled = isEmpty;
exportBtn.classList.toggle("btn-disabled", isEmpty);
const parent = addSection.parentElement;
if (isEmpty) {
if (parent.children[0] !== addSection) parent.insertBefore(addSection, listSection);
} else {
if (parent.children[0] !== listSection) parent.insertBefore(listSection, addSection);
}
if (isEmpty) {
configList.innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<path d="M3 10h18"/>
<circle cx="7" cy="14.5" r="0.6" fill="currentColor"/>
<path d="M11 14.5h6"/>
</svg>
</div>
<div class="empty-text">${t("emptyStateTitle")}</div>
<div class="empty-sub">${t("emptyStateSub")}</div>
</div>
`;
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
? `<span class="tag-disabled">${t("tagDisabled")}</span>`
: "";
const delayTag = cfg.delayMs && Number(cfg.delayMs) > 300
? `<span class="tag-delay">${t("tagDelay", String(cfg.delayMs))}</span>`
: "";
const autoSubmitText = cfg.autoSubmit ? t("yesShort") : t("noShort");
info.innerHTML = `
<div class="config-domain">${displayName} ${enabledTag} ${delayTag}</div>
${showDomain ? `<div class="config-domain-sub">${showDomain}</div>` : ""}
<div class="config-detail">${t("configDetailUser", escapeHtml(cfg.username), autoSubmitText)}</div>
`;
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;
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 = {
id: editModalEditingId,
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();
const idx = configs.findIndex((c) => c.id === editModalEditingId);
if (idx !== -1) {
configs[idx] = config;
}
await saveConfigs(configs);
editConfigModal.classList.remove("show");
editModalEditingId = null;
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
showToast(t("toastUpdated"));
});
// 编辑配置弹窗:取消
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 {
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 hostname = new URL(tab.url).hostname;
if (!hostname) {
showToast(t("toastNoDomain"));
return;
}
domainInput.value = hostname;
saveDraft();
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)) {
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"
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" });
if (!resp.hasVault) {
showLockScreen("setup");
} else if (!resp.unlocked) {
showLockScreen("unlock");
} else {
showMainUI();
loadDraft();
renderEnvSelect();
renderConfigList();
}
} catch (err) {
console.warn("[AutoLogin]", t("logGetLockStatusFail"), err);
lockWarn.textContent = t("lockWarnNoService");
lockWarn.style.display = "block";
showLockScreen("setup");
}
}
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"));
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 () => {
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();
});