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