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 = `
`;
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();
});