Greasy Fork

Greasy Fork is available in English.

自动登录助手

维护站点登录配置,并在检测到登录表单时自动登录。

当前为 2026-04-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         自动登录助手
// @namespace    https://local.autologin.helper
// @version      0.3.0
// @description  维护站点登录配置,并在检测到登录表单时自动登录。
// @match        *://*/*
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      *
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  if (window.top !== window.self) {
    return;
  }

  const STORAGE_KEY = 'siteConfigs';
  const CONFIG_META_KEY = 'siteConfigsMeta';
  const LOG_STORAGE_KEY = 'runtimeLogs';
  const LOG_ENABLED_KEY = 'runtimeLogsEnabled';
  const SYNC_SETTINGS_KEY = 'syncSettings';
  const STYLE_ID = 'alh-style';
  const PANEL_ID = 'alh-panel-root';
  const PROMPT_ID = 'alh-prompt-root';
  const LOG_ID = 'alh-log-root';
  const INTERNAL_UI_SELECTORS = `#${PANEL_ID}, #${PROMPT_ID}, #${LOG_ID}`;
  const DEFAULT_MODE = 'auto';
  const DEFAULT_WEBDAV_DIR = 'auto-login-helper';
  const DEFAULT_WEBDAV_FILENAME = 'auto-login-helper.json';
  const MAX_LOGS = 200;
  const SYNC_VERSION = 1;
  const AUTO_SYNC_COOLDOWN_MS = 5 * 60 * 1000;

  const state = {
    configs: loadConfigs(),
    configMeta: loadConfigMeta(),
    activeConfigId: null,
    autoAttempted: false,
    logs: loadLogs(),
    drag: null,
    picker: null,
    actionLocks: {},
    attemptCounters: {},
    logEnabled: loadLogEnabled(),
    missingFieldWarnings: {},
    managerTab: 'configs',
    managerEditingConfigId: null,
    managerFollowMatchedConfig: true,
    selectedConfigIds: [],
    syncSettings: loadSyncSettings(),
    syncInProgress: false,
    syncStatus: {
      level: 'idle',
      message: '尚未执行同步。',
      time: '',
    },
    menuExpanded: false,
    menuCommandIds: [],
  };
  state.syncStatus = buildInitialSyncStatus(state.syncSettings);

  bootstrap();

  function bootstrap() {
    injectStyles();
    registerMenu();
    waitForPageReady(() => {
      if (state.logEnabled) {
        renderLogPanel();
        log('info', '页面已加载,开始检测登录表单。');
      }
      maybeAutoSyncOnLoad();
      runAutoLoginFlow();
      observePageChanges();
    });
  }

  function registerMenu() {
    if (state.menuCommandIds && state.menuCommandIds.length) {
      state.menuCommandIds.forEach(id => GM_unregisterMenuCommand(id));
    }
    state.menuCommandIds = [];

    state.menuCommandIds.push(GM_registerMenuCommand('打开自动登录管理器', openManager));
    state.menuCommandIds.push(GM_registerMenuCommand('快速添加当前站点(自动提交-Bitwarden)', () => quickAddCurrentSite('auto_bitwarden')));
    state.menuCommandIds.push(GM_registerMenuCommand('快速添加当前站点(全自动-脚本账号密码)', () => quickAddCurrentSite('auto_script')));

    if (state.menuExpanded) {
      state.menuCommandIds.push(GM_registerMenuCommand('折叠更多选项 ▲', toggleAdvancedMenu, { autoClose: false }));
      state.menuCommandIds.push(GM_registerMenuCommand('立即同步 WebDAV', () => {
        syncWithWebDav('two-way', { manual: true });
      }));
      state.menuCommandIds.push(GM_registerMenuCommand('打开运行日志', toggleLogPanel));
      state.menuCommandIds.push(GM_registerMenuCommand('关闭运行日志', disableLogPanel));
    } else {
      state.menuCommandIds.push(GM_registerMenuCommand('展开更多选项 ▼', toggleAdvancedMenu, { autoClose: false }));
    }
  }

  function toggleAdvancedMenu() {
    state.menuExpanded = !state.menuExpanded;
    registerMenu();
  }

  function loadConfigs() {
    const raw = GM_getValue(STORAGE_KEY, '[]');
    try {
      const parsed = JSON.parse(raw);
      const { configs, changed } = sanitizeConfigCollection(Array.isArray(parsed) ? parsed : []);
      if (changed) {
        GM_setValue(STORAGE_KEY, JSON.stringify(configs));
      }
      return configs;
    } catch (error) {
      console.warn('[自动登录]', '解析配置失败:', error);
      return [];
    }
  }

  function saveConfigs() {
    saveConfigsWithOptions();
  }

  function saveConfigsWithOptions(options = {}) {
    const { configs, changed } = sanitizeConfigCollection(state.configs);
    if (changed) {
      state.configs = configs;
      if (!state.configs.some((item) => item.id === state.activeConfigId)) {
        state.activeConfigId = state.configs[0] ? state.configs[0].id : null;
      }
      if (!state.configs.some((item) => item.id === state.managerEditingConfigId)) {
        state.managerEditingConfigId = state.activeConfigId;
      }
      state.selectedConfigIds = state.selectedConfigIds.filter((id) => state.configs.some((item) => item.id === id));
      if (!state.selectedConfigIds.length && state.activeConfigId) {
        state.selectedConfigIds = [state.activeConfigId];
      }
    }
    GM_setValue(STORAGE_KEY, JSON.stringify(state.configs));
    if (options.markUpdatedAt !== false) {
      state.configMeta.updatedAt = options.updatedAt || new Date().toISOString();
    } else if (options.updatedAt) {
      state.configMeta.updatedAt = options.updatedAt;
    }
    saveConfigMeta();
    if (options.scheduleSync !== false) {
      scheduleAutoSyncOnSave();
    }
  }

  function sanitizeConfigCollection(configs) {
    const source = Array.isArray(configs) ? configs : [];
    const seen = new Set();
    let changed = false;
    const next = source.map((item) => {
      const config = item && typeof item === 'object' ? { ...item } : {};
      const rawId = typeof config.id === 'string' ? config.id.trim() : '';
      const id = rawId && !seen.has(rawId) ? rawId : createId();
      if (id !== rawId) changed = true;
      seen.add(id);
      config.id = id;
      return config;
    });
    return { configs: next, changed };
  }

  function loadLogs() {
    const raw = GM_getValue(LOG_STORAGE_KEY, '[]');
    try {
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed : [];
    } catch (error) {
      return [];
    }
  }

  function loadLogEnabled() {
    return GM_getValue(LOG_ENABLED_KEY, false) === true;
  }

  function saveLogEnabled() {
    GM_setValue(LOG_ENABLED_KEY, state.logEnabled === true);
  }

  function saveLogs() {
    GM_setValue(LOG_STORAGE_KEY, JSON.stringify(state.logs.slice(-MAX_LOGS)));
  }

  function loadConfigMeta() {
    const raw = GM_getValue(CONFIG_META_KEY, '{}');
    try {
      const parsed = JSON.parse(raw);
      return parsed && typeof parsed === 'object'
        ? {
            updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
          }
        : { updatedAt: '' };
    } catch (error) {
      return { updatedAt: '' };
    }
  }

  function saveConfigMeta() {
    GM_setValue(CONFIG_META_KEY, JSON.stringify(state.configMeta || { updatedAt: '' }));
  }

  function loadSyncSettings() {
    const raw = GM_getValue(SYNC_SETTINGS_KEY, '{}');
    try {
      const parsed = JSON.parse(raw);
      return normalizeSyncSettings(parsed);
    } catch (error) {
      return normalizeSyncSettings({});
    }
  }

  function saveSyncSettings() {
    GM_setValue(SYNC_SETTINGS_KEY, JSON.stringify(state.syncSettings));
  }

  function normalizeSyncSettings(value) {
    const source = value && typeof value === 'object' ? value : {};
    return {
      enabled: source.enabled === true,
      webdavUrl: String(source.webdavUrl || '').trim(),
      syncDirectory: String(source.syncDirectory || DEFAULT_WEBDAV_DIR).trim() || DEFAULT_WEBDAV_DIR,
      fileName: String(source.fileName || DEFAULT_WEBDAV_FILENAME).trim() || DEFAULT_WEBDAV_FILENAME,
      username: String(source.username || ''),
      password: String(source.password || ''),
      autoSyncOnLoad: source.autoSyncOnLoad !== false,
      autoSyncOnSave: source.autoSyncOnSave !== false,
      lastAutoSyncAt: String(source.lastAutoSyncAt || ''),
      lastSyncAt: String(source.lastSyncAt || ''),
      lastError: String(source.lastError || ''),
    };
  }

  function maybeAutoSyncOnLoad() {
    if (!state.syncSettings.enabled || !state.syncSettings.autoSyncOnLoad || !state.syncSettings.webdavUrl) {
      return;
    }
    const lastAuto = Date.parse(state.syncSettings.lastAutoSyncAt || '');
    if (Number.isFinite(lastAuto) && Date.now() - lastAuto < AUTO_SYNC_COOLDOWN_MS) {
      return;
    }
    state.syncSettings.lastAutoSyncAt = new Date().toISOString();
    saveSyncSettings();
    syncWithWebDav('two-way', { manual: false });
  }

  function scheduleAutoSyncOnSave() {
    if (!state.syncSettings.enabled || !state.syncSettings.autoSyncOnSave || !state.syncSettings.webdavUrl) {
      return;
    }
    window.setTimeout(() => {
      syncWithWebDav('push', { manual: false });
    }, 0);
  }

  function waitForPageReady(callback) {
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
      callback();
      return;
    }
    window.addEventListener('DOMContentLoaded', callback, { once: true });
  }

  function observePageChanges() {
    const observer = new MutationObserver(debounce(() => {
      if (!document.hidden) {
        log('debug', '页面发生变化,重新检测登录表单。');
        runAutoLoginFlow();
      }
    }, 600));

    observer.observe(document.documentElement || document.body, {
      childList: true,
      subtree: true,
    });
  }

  function runAutoLoginFlow() {
    const matches = getMatchingConfigs();
    log('debug', `当前匹配到 ${matches.length} 个配置。`);
    if (matches.length) {
      const config = normalizeConfig(matches[0]);
      if (isLoggedIn(config)) {
        log('info', `站点 ${config.name || config.id} 已判定为登录状态,跳过自动登录。`);
        removePrompt();
        return;
      }
      const activeStep = resolveActiveStep(config);
      if (!activeStep) {
        warnMissingFields(config);
        debugCandidateElements(config);
        removePrompt();
        return;
      }
      if (config.mode === 'prompt') {
        const promptText = config.credentialSource === 'bitwarden'
          ? `检测到${getStepTypeLabel(activeStep.step.type)},等待 Bitwarden 或手动填充后执行。`
          : '已检测到登录表单,点击即可自动填充并提交。';
        showPrompt({
          title: `准备登录:${config.name || location.host}`,
          text: promptText,
          actions: [
            { label: '执行', onClick: () => executeStep(config, activeStep, true) },
            { label: '编辑', onClick: openManager },
          ],
        });
        return;
      }
      if (config.mode === 'auto' && !state.autoAttempted) {
        state.autoAttempted = true;
        log('info', `站点 ${config.name || config.id} 进入自动执行流程,当前页面类型:${getStepTypeLabel(activeStep.step.type)}。`);
        executeStep(config, activeStep, false);
      }
      return;
    }

    state.autoAttempted = false;

    const guess = guessLoginElements();
    if (!guess.password) {
      log('debug', '当前页面未检测到密码框。');
      removePrompt();
      return;
    }

    log('info', '检测到疑似登录页面,但还没有保存配置。');

    showPrompt({
      title: `检测到登录页:${location.host}`,
      text: '当前站点还没有保存配置。请选择该站点的登录执行方式。',
      actions: [
        { label: '全自动(脚本账号密码)', onClick: () => quickAddCurrentSite('auto_script') },
        { label: '自动提交(Bitwarden)', onClick: () => quickAddCurrentSite('auto_bitwarden') },
        { label: '忽略', onClick: removePrompt },
      ],
    });
  }

  function getMatchingConfigs() {
    const matches = state.configs.filter((config) => config.enabled !== false && isMatch(config));
    if (!matches.length && state.configs.length) {
      debugConfigMismatchReasons();
    }
    return matches;
  }

  function debugConfigMismatchReasons() {
    state.configs.forEach((config) => {
      if (config.enabled === false) {
        log('debug', `未匹配配置 ${config.name || config.id}:该配置已禁用。`);
        return;
      }
      log('debug', `未匹配配置 ${config.name || config.id}:${getMismatchReason(config)}`);
    });
  }

  function getMismatchReason(config) {
    const url = location.href;
    const host = location.host;
    const path = `${location.host}${location.pathname}`;

    if (config.matchType === 'regex') {
      try {
        return new RegExp(config.matchValue).test(url)
          ? '正则已匹配,但被其他条件过滤。'
          : `当前 URL ${url} 不匹配正则 ${config.matchValue}`;
      } catch (error) {
        return `正则无效:${error.message}`;
      }
    }

    if (config.matchType === 'url') {
      return path.includes(config.matchValue || '')
        ? 'URL 已匹配,但被其他条件过滤。'
        : `当前路径 ${path} 不包含 ${config.matchValue || '(空)'}`;
    }

    return host === config.matchValue || host.endsWith(`.${config.matchValue}`)
      ? '域名已匹配,但被其他条件过滤。'
      : `当前域名 ${host} 不匹配 ${config.matchValue || '(空)'}`;
  }

  function isMatch(config) {
    const url = location.href;
    const host = location.host;
    const path = `${location.host}${location.pathname}`;

    if (config.matchType === 'regex') {
      try {
        const matched = new RegExp(config.matchValue).test(url);
        if (matched) log('debug', `正则匹配成功:${config.matchValue}`);
        return matched;
      } catch (error) {
        log('error', `正则匹配失败:${error.message}`);
        return false;
      }
    }
    if (config.matchType === 'url') {
      const matched = path.includes(config.matchValue || '');
      if (matched) log('debug', `URL 匹配成功:${config.matchValue}`);
      return matched;
    }
    const matched = host === config.matchValue || host.endsWith(`.${config.matchValue}`);
    if (matched) log('debug', `域名匹配成功:${config.matchValue}`);
    return matched;
  }

  function isLoggedIn(config) {
    return false;
  }

  function normalizeConfig(config) {
    const normalized = { ...config };
    normalized.mode = normalizeMode(normalized.mode);
    normalized.credentialSource = normalized.credentialSource || 'bitwarden';
    normalized.steps = Array.isArray(normalized.steps) && normalized.steps.length
      ? normalized.steps.map((step, index) => normalizeStep(step, index))
      : [normalizeLegacyStep(normalized)];
    return normalized;
  }

  function warnMissingFields(config) {
    const key = `${config.id || config.matchValue}:${location.pathname}`;
    const now = Date.now();
    const last = state.missingFieldWarnings[key] || 0;
    if (now - last < 5000) return;
    state.missingFieldWarnings[key] = now;
    log('warn', `配置 ${config.name || config.id} 已匹配,但未找到当前阶段需要的字段。`);
  }

  function debugCandidateElements(config) {
    const key = `${config.id || config.matchValue}:${location.pathname}:debug`;
    const now = Date.now();
    const last = state.missingFieldWarnings[key] || 0;
    if (now - last < 5000) return;
    state.missingFieldWarnings[key] = now;
    const docs = getSearchableDocuments();
    docs.forEach((doc, index) => {
      const inputs = collectCandidateElements(doc, 'input, textarea').slice(0, 8);
      const buttons = collectCandidateElements(doc, 'button, input[type="submit"], [role="button"]').slice(0, 8);
      log('debug', `候选调试 文档${index + 1} 输入框=${inputs.length} 按钮=${buttons.length}`);
      inputs.forEach((element, itemIndex) => {
        log('debug', `输入候选${itemIndex + 1}: ${describeElement(element)}`);
      });
      buttons.forEach((element, itemIndex) => {
        log('debug', `按钮候选${itemIndex + 1}: ${describeElement(element)}`);
      });
    });
  }

  function normalizeStep(step, index) {
    return {
      id: step.id || `step_${index + 1}`,
      name: step.name || '',
      type: step.type || inferStepType(step),
      usernameSelector: step.usernameSelector || '',
      passwordSelector: step.passwordSelector || '',
      otpSelector: step.otpSelector || '',
      submitSelector: step.submitSelector || '',
      waitUntil: step.waitUntil || inferWaitUntil(step),
      autoSubmit: step.autoSubmit !== false,
      fillDelay: Number(step.fillDelay || 400),
      maxAttempts: Number(step.maxAttempts || 2),
      autoDetectSubmit: step.autoDetectSubmit !== false,
    };
  }

  function normalizeLegacyStep(config) {
    return normalizeStep({
      id: 'legacy',
      name: '',
      type: config.passwordSelector && config.usernameSelector ? 'credentials' : config.passwordSelector ? 'password' : 'username',
      usernameSelector: config.usernameSelector || '',
      passwordSelector: config.passwordSelector || '',
      submitSelector: config.submitSelector || '',
      waitUntil: config.credentialSource === 'bitwarden' ? inferWaitUntil(config) : 'readyNow',
      autoSubmit: true,
      fillDelay: 400,
      maxAttempts: 2,
      autoDetectSubmit: true,
    }, 0);
  }

  function inferStepType(step) {
    if (step.otpSelector) return 'otp';
    if (step.passwordSelector && step.usernameSelector) return 'credentials';
    if (step.passwordSelector) return 'password';
    return 'username';
  }

  function inferWaitUntil(step) {
    if (step.type === 'credentials') return 'usernameAndPasswordFilled';
    if (step.type === 'password') return 'passwordFilled';
    if (step.type === 'username') return 'usernameFilled';
    if (step.type === 'otp') return 'otpFilled';
    if (step.otpSelector) return 'otpFilled';
    if (step.passwordSelector && step.usernameSelector) return 'usernameAndPasswordFilled';
    if (step.passwordSelector) return 'passwordFilled';
    return 'usernameFilled';
  }

  function resolveActiveStep(config) {
    for (const step of config.steps) {
      const stepContext = resolveStepContext(step);
      const visible = stepContext.username || stepContext.password || stepContext.otp;
      if (stepContext.ready || visible) {
        log('debug', `已定位到阶段:${getStepLabel(step)}。`);
        return { step, stepContext };
      }
    }
    return null;
  }

  function resolveStepContext(step) {
    const context = {
      username: null,
      password: null,
      otp: null,
      submit: null,
      ready: false,
      sourceDocument: document,
    };
    try {
      context.username = step.usernameSelector ? queryAcrossDocuments(step.usernameSelector) : null;
      context.password = step.passwordSelector ? queryAcrossDocuments(step.passwordSelector) : null;
      context.otp = step.otpSelector ? queryAcrossDocuments(step.otpSelector) : null;
      context.submit = step.submitSelector ? queryAcrossDocuments(step.submitSelector) : null;
    } catch (error) {
      log('error', `阶段 ${getStepLabel(step)} 选择器解析失败:${error.message}`);
      return context;
    }
    const needsUsername = step.type === 'username' || step.type === 'credentials' || step.waitUntil === 'usernameFilled' || step.waitUntil === 'usernameAndPasswordFilled';
    const needsPassword = step.type === 'password' || step.type === 'credentials' || step.waitUntil === 'passwordFilled' || step.waitUntil === 'usernameAndPasswordFilled';
    const needsOtp = step.type === 'otp' || step.waitUntil === 'otpFilled';
    if ((!context.username && needsUsername) || (!context.password && needsPassword) || (!context.otp && needsOtp) || (!context.submit && step.autoDetectSubmit !== false)) {
      const guessed = guessLoginElementsDeep();
      if (!context.username && needsUsername) context.username = guessed.username;
      if (!context.password && needsPassword) context.password = guessed.password;
      if (!context.otp && needsOtp) context.otp = guessed.otp;
      if (!context.submit && !step.submitSelector) context.submit = guessed.submit;
    }
    if (!context.submit && step.autoDetectSubmit !== false) {
      context.submit = guessSubmitElement(step, context);
    }
    context.ready = (!needsUsername || Boolean(context.username))
      && (!needsPassword || Boolean(context.password))
      && (!needsOtp || Boolean(context.otp));
    log('debug', `阶段 ${getStepLabel(step)} 字段解析:用户名=${Boolean(context.username)},密码=${Boolean(context.password)},验证码=${Boolean(context.otp)},按钮=${Boolean(context.submit)}`);
    return context;
  }

  function executeStep(config, activeStep, fromPrompt) {
    const { step } = activeStep;
    if (hasReachedAttemptLimit(config, step)) {
      log('warn', `阶段 ${getStepLabel(step)} 已达到最大执行次数 ${step.maxAttempts},停止继续执行。`);
      return;
    }
    const context = resolveStepContext(step);
    if (!context.ready) {
      notify('当前阶段字段不完整,请检查选择器配置。');
      log('warn', `执行失败:阶段 ${getStepLabel(step)} 缺少必要字段。`);
      return;
    }
    if (config.credentialSource === 'bitwarden') {
      watchForExternalFill(config, step, context, fromPrompt);
      return;
    }
    fillStepCredentials(config, step, context);
    queueStepSubmit(config, step, context, fromPrompt);
  }

  function fillStepCredentials(config, step, context) {
    log('info', `开始填充阶段 ${getStepLabel(step)}。`);
    if (step.usernameSelector) fillField(context.username, config.username || '');
    if (step.passwordSelector) fillField(context.password, config.password || '');
    if (step.otpSelector && config.otpValue) fillField(context.otp, config.otpValue || '');
  }

  function queueStepSubmit(config, step, context, fromPrompt) {
    log('debug', `阶段 ${getStepLabel(step)} 将在 ${step.fillDelay || 300}ms 后提交。`);
    window.setTimeout(() => {
      submitStep(config, step, context);
    }, step.fillDelay || 300);
    if (fromPrompt) removePrompt();
  }

  function watchForExternalFill(config, step, context, fromPrompt) {
    const key = buildActionKey(config, step);
    if (state.actionLocks[key]) {
      log('debug', `阶段 ${getStepLabel(step)} 已在等待外部填充,跳过重复监听。`);
      return;
    }
    state.actionLocks[key] = { waiting: true, submitted: false };
    log('info', `阶段 ${getStepLabel(step)} 等待 Bitwarden 或手动填充。`);
    const startedAt = Date.now();

    const tick = () => {
      const latestContext = resolveStepContext(step);
      if (!latestContext.ready) {
        if (Date.now() - startedAt < 180000) {
          window.setTimeout(tick, 600);
        } else {
          log('warn', `阶段 ${getStepLabel(step)} 等待超时。`);
          delete state.actionLocks[key];
        }
        return;
      }
      if (!isStepFilled(step, latestContext)) {
        if (Date.now() - startedAt < 180000) {
          window.setTimeout(tick, 600);
        } else {
          log('warn', `阶段 ${getStepLabel(step)} 在超时时间内未检测到填充值。`);
          delete state.actionLocks[key];
        }
        return;
      }
      if (state.actionLocks[key] && !state.actionLocks[key].submitted) {
        state.actionLocks[key].submitted = true;
        log('info', `阶段 ${getStepLabel(step)} 已检测到外部填充,准备自动提交。`);
        queueStepSubmit(config, step, latestContext, fromPrompt);
        window.setTimeout(() => {
          delete state.actionLocks[key];
        }, 1200);
      }
    };

    tick();
  }

  function isStepFilled(step, context) {
    if (step.waitUntil === 'readyNow') return true;
    if (step.waitUntil === 'usernameFilled') return hasValue(context.username);
    if (step.waitUntil === 'passwordFilled') return hasValue(context.password);
    if (step.waitUntil === 'usernameAndPasswordFilled') return hasValue(context.username) && hasValue(context.password);
    if (step.waitUntil === 'otpFilled') return hasValue(context.otp);
    return false;
  }

  function submitStep(config, step, context) {
    increaseAttemptCounter(config, step);
    if (step.autoSubmit === false) {
      log('info', `阶段 ${getStepLabel(step)} 已满足条件,但配置为不自动提交。`);
      return;
    }
    log('info', `开始提交阶段 ${getStepLabel(step)}。`);
    if (context.submit) {
      log('debug', `${step.submitSelector ? '使用配置' : '使用自动推断'}的提交按钮:${buildSelector(context.submit)}`);
      triggerClick(context.submit);
      return;
    }
    const baseField = context.otp || context.password || context.username;
    if (baseField && baseField.form) {
      log('debug', '使用表单 requestSubmit / submit 提交。');
      baseField.form.requestSubmit ? baseField.form.requestSubmit() : baseField.form.submit();
      return;
    }
    log('debug', '使用回车键触发提交。');
    triggerEnter(baseField);
  }

  function buildActionKey(config, step) {
    return `${config.id || config.matchValue}:${step.id}:${location.pathname}`;
  }

  function getAttemptKey(config, step) {
    return `${config.id || config.matchValue}:${step.id}:${location.origin}${location.pathname}`;
  }

  function hasReachedAttemptLimit(config, step) {
    const key = getAttemptKey(config, step);
    const count = state.attemptCounters[key] || 0;
    return count >= (step.maxAttempts || 2);
  }

  function increaseAttemptCounter(config, step) {
    const key = getAttemptKey(config, step);
    state.attemptCounters[key] = (state.attemptCounters[key] || 0) + 1;
    log('info', `阶段 ${getStepLabel(step)} 第 ${state.attemptCounters[key]} 次执行。`);
  }

  function getStepLabel(step) {
    return step.name || getStepTypeLabel(step.type);
  }

  function hasValue(element) {
    return Boolean(element && String(element.value || '').trim());
  }

  function getStepTypeLabel(type) {
    if (type === 'credentials') return '用户名和密码同页';
    if (type === 'username') return '用户名页';
    if (type === 'password') return '密码页';
    if (type === 'otp') return '验证码页';
    return type || '未知阶段';
  }

  function fillField(element, value) {
    if (!element) {
      log('debug', '跳过空字段填充。');
      return;
    }
    element.focus();
    const nativeSetter = Object.getOwnPropertyDescriptor(element.__proto__, 'value')?.set;
    if (nativeSetter) {
      nativeSetter.call(element, value);
    } else {
      element.value = value;
    }
    element.dispatchEvent(new Event('input', { bubbles: true }));
    element.dispatchEvent(new Event('change', { bubbles: true }));
    element.blur();
    log('debug', `已填充字段:${buildSelector(element)}`);
  }

  function guessSubmitElement(step, context) {
    const base = context.otp || context.password || context.username;
    const scope = resolveScopeElement(base);
    const candidate = findLikelySubmit(scope) || findLikelySubmit(document);
    if (candidate) {
      log('debug', `自动推断提交按钮成功:${buildSelector(candidate)}`);
    }
    return candidate;
  }

  function resolveScopeElement(base) {
    if (!base) return document;
    return base.closest('form') || base.closest('[role="dialog"]') || base.closest('section') || base.parentElement || document;
  }

  function findLikelySubmit(scope) {
    if (!scope || !scope.querySelectorAll) return null;
    const selectors = [
      'button[type="submit"]',
      'input[type="submit"]',
      'button',
      '[role="button"]',
      'a',
      'div[onclick]',
      'span[onclick]',
      '.login',
      '.signin',
      '.submit',
    ];
    const elements = collectCandidateElements(scope, selectors.join(','))
      .filter((element) => isVisible(element) && !isInternalUiElement(element));
    if (!elements.length) return null;

    const scored = elements.map((element) => ({ element, score: scoreSubmitCandidate(element) }))
      .filter((item) => item.score > 0)
      .sort((a, b) => b.score - a.score);

    return scored.length ? scored[0].element : null;
  }

  function scoreSubmitCandidate(element) {
    if (!(element instanceof Element)) return 0;
    if (isInternalUiElement(element)) return 0;
    const text = `${element.textContent || ''} ${element.getAttribute('value') || ''} ${element.getAttribute('aria-label') || ''}`.trim().toLowerCase();
    const type = (element.getAttribute('type') || '').toLowerCase();
    let score = 0;

    if (type === 'submit') score += 10;
    if (element.matches('button')) score += 4;
    if (element.getAttribute('role') === 'button') score += 3;
    if (element.matches('a, div[onclick], span[onclick]')) score += 2;
    if (/登录|登入|sign in|log in|continue|next|submit|verify|验证/.test(text)) score += 8;
    if (/注册|忘记|help|cancel|close/.test(text)) score -= 6;

    const rect = element.getBoundingClientRect();
    if (rect.width > 30 && rect.height > 20) score += 2;
    if (rect.width > 500 || rect.height > 200) score -= 8;

    return score;
  }

  function isInternalUiElement(element) {
    return Boolean(element && element.closest && element.closest(INTERNAL_UI_SELECTORS));
  }

  function triggerClick(element) {
    element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
    element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
    element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
    element.click();
  }

  function triggerEnter(element) {
    if (!element) return;
    element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
    element.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
  }

  function guessLoginElements() {
    const password = firstVisible('input[type="password"]');
    const username = firstVisible([
      'input[type="email"]',
      'input[name*="user" i]',
      'input[name*="login" i]',
      'input[name*="account" i]',
      'input[type="text"]',
    ].join(','));
    const submit = firstVisible([
      'button[type="submit"]',
      'input[type="submit"]',
      'button',
    ].join(','));
    return { username, password, submit };
  }

  function guessLoginElementsDeep() {
    const documents = getSearchableDocuments();
    for (const doc of documents) {
      const password = firstVisibleInRoot(doc, 'input[type="password"]');
      const otp = firstVisibleInRoot(doc, [
        'input[inputmode="numeric"]',
        'input[name*="otp" i]',
        'input[name*="code" i]',
        'input[autocomplete="one-time-code"]',
      ].join(','));
      const username = firstVisibleInRoot(doc, [
        'input[type="email"]',
        'input[name*="user" i]',
        'input[name*="login" i]',
        'input[name*="account" i]',
        'input[type="text"]',
      ].join(','));
      const submit = findLikelySubmit(doc);
      if (username || password || otp || submit) {
        return { username, password, otp, submit };
      }
    }
    return { username: null, password: null, otp: null, submit: null };
  }

  function getSearchableDocuments() {
    const docs = [document];
    const frames = Array.from(document.querySelectorAll('iframe'));
    frames.forEach((frame) => {
      try {
        if (frame.contentDocument) docs.push(frame.contentDocument);
      } catch (error) {
        // ignore cross-origin frames
      }
    });
    return docs;
  }

  function queryAcrossDocuments(selector) {
    const docs = getSearchableDocuments();
    for (const doc of docs) {
      try {
        const element = firstVisibleInRoot(doc, selector);
        if (element && isVisible(element)) return element;
      } catch (error) {
        // ignore invalid selector for this document pass
      }
    }
    return null;
  }

  function firstVisibleInRoot(root, selector) {
    const elements = collectCandidateElements(root, selector);
    return elements.find((element) => isVisible(element) && !isInternalUiElement(element)) || null;
  }

  function collectCandidateElements(root, selector) {
    const results = [];
    const visited = new Set();

    const walk = (currentRoot) => {
      if (!currentRoot || visited.has(currentRoot)) return;
      visited.add(currentRoot);
      if (currentRoot.querySelectorAll) {
        results.push(...Array.from(currentRoot.querySelectorAll(selector)).filter((element) => !isInternalUiElement(element)));
        const allElements = currentRoot.querySelectorAll('*');
        allElements.forEach((element) => {
          if (element.shadowRoot) {
            walk(element.shadowRoot);
          }
        });
      }
    };

    walk(root);
    return results;
  }

  function firstVisible(selector) {
    const elements = Array.from(document.querySelectorAll(selector));
    return elements.find((element) => isVisible(element) && !isInternalUiElement(element)) || null;
  }

  function isVisible(element) {
    if (!element) return false;
    const style = window.getComputedStyle(element);
    const rect = element.getBoundingClientRect();
    return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
  }

  function quickAddCurrentSite(preset = 'auto_bitwarden') {
    const guess = guessLoginElementsDeep();
    const presetType = normalizeQuickPreset(preset);
    const mode = 'auto';
    const credentialSource = presetType === 'auto_script' ? 'script' : 'bitwarden';
    const usernameValue = credentialSource === 'script'
      ? (window.prompt('请输入该站点用户名 / 邮箱', guess.username ? guess.username.value || '' : '') || '')
      : '';
    const passwordValue = credentialSource === 'script'
      ? (window.prompt('请输入该站点密码', guess.password ? guess.password.value || '' : '') || '')
      : '';
    const stepType = guess.otp && !guess.password && !guess.username
      ? 'otp'
      : guess.password && guess.username
        ? 'credentials'
        : guess.password
          ? 'password'
          : guess.username
            ? 'username'
            : 'password';
    const waitUntil = credentialSource === 'bitwarden' ? inferWaitUntil({ type: stepType }) : 'readyNow';

    const config = {
      id: createId(),
      name: location.host,
      enabled: true,
      matchType: 'host',
      matchValue: location.host,
      mode,
      credentialSource: normalizeCredentialSource(credentialSource),
      usernameSelector: guess.username ? buildSelector(guess.username) : '',
      passwordSelector: guess.password ? buildSelector(guess.password) : (stepType === 'password' || stepType === 'credentials' ? 'input[type="password"]' : ''),
      submitSelector: guess.submit ? buildSelector(guess.submit) : '',
      username: usernameValue,
      password: passwordValue,
      otpValue: guess.otp ? String(guess.otp.value || '') : '',
      steps: [
        {
          id: 'step_1',
          name: '',
          type: stepType,
          usernameSelector: guess.username ? buildSelector(guess.username) : '',
          passwordSelector: guess.password ? buildSelector(guess.password) : (stepType === 'password' || stepType === 'credentials' ? 'input[type="password"]' : ''),
          otpSelector: guess.otp ? buildSelector(guess.otp) : '',
          submitSelector: guess.submit ? buildSelector(guess.submit) : '',
          waitUntil,
          autoSubmit: true,
          fillDelay: 400,
          maxAttempts: 2,
          autoDetectSubmit: true,
        },
      ],
    };

    const index = state.configs.findIndex((item) => item.matchType === 'host' && item.matchValue === location.host);
    if (index >= 0) {
      config.id = state.configs[index].id;
      state.configs[index] = config;
    } else {
      state.configs.push(config);
    }
    saveConfigs();
    const modeLabel = '自动执行';
    const sourceLabel = credentialSource === 'script' ? '脚本账号密码' : 'Bitwarden/手动填充';
    notify(`配置已保存:${modeLabel} + ${sourceLabel}`);
    log('info', `已快速保存站点配置:${location.host},模式=${mode},凭据来源=${credentialSource}`);
    openManager(config.id);
  }

  function normalizeQuickPreset(value) {
    const preset = String(value || '').trim().toLowerCase();
    if (preset === 'manual') return 'auto_bitwarden';
    if (preset === 'auto_script') return 'auto_script';
    return 'auto_bitwarden';
  }

  function createId() {
    return `cfg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  }

  function normalizeMode(value) {
    const mode = String(value || '').trim().toLowerCase();
    if (mode === 'auto') return mode;
    return 'prompt';
  }

  function normalizeCredentialSource(value) {
    const source = String(value || '').trim().toLowerCase();
    return source === 'bitwarden' ? 'bitwarden' : 'script';
  }

  function buildSelector(element) {
    if (!element) return '';
    if (element.id) return `#${cssEscape(element.id)}`;
    const parts = [element.tagName.toLowerCase()];
    if (element.name) parts.push(`[name="${escapeAttribute(element.name)}"]`);
    if (element.type) parts.push(`[type="${escapeAttribute(element.type)}"]`);
    if (element.classList.length) {
      const firstClass = Array.from(element.classList).find(Boolean);
      if (firstClass) parts.push(`.${cssEscape(firstClass)}`);
    }
    return parts.join('');
  }

  function cssEscape(value) {
    return String(value).replace(/([ #;?%&,.+*~\':"!^$\[\]()=>|\/])/g, '\\$1');
  }

  function escapeAttribute(value) {
    return String(value).replace(/"/g, '\\"');
  }

  function openManager(preselectId) {
    removePrompt();
    removeManager();
    const preferredId = preselectId || findPreferredManagerConfigId() || (state.configs[0] && state.configs[0].id) || null;
    state.activeConfigId = preferredId;
    state.managerEditingConfigId = preferredId;
    state.managerFollowMatchedConfig = !preselectId;
    state.selectedConfigIds = preferredId ? [preferredId] : [];
    if (preselectId) state.managerTab = 'configs';

    const root = document.createElement('div');
    root.id = PANEL_ID;
    root.innerHTML = managerMarkup();
    root.style.display = 'block';
    document.body.appendChild(root);
    makeDraggable(root.querySelector('.alh-panel'), root.querySelector('.alh-header'));
    const panel = root.querySelector('.alh-panel');
    if (panel) {
      panel.style.position = 'fixed';
      panel.style.left = '20px';
      panel.style.top = '20px';
      panel.style.right = 'auto';
      panel.style.bottom = 'auto';
      panel.style.margin = '0';
    }

    root.querySelector('.alh-close').addEventListener('click', removeManager);
    renderManager();
  }

  function removeManager() {
    const existing = document.getElementById(PANEL_ID);
    if (existing) existing.remove();
  }

  function createEmptyConfig() {
    return {
      id: createId(),
      name: location.host,
      enabled: true,
      matchType: 'host',
      matchValue: location.host,
      mode: DEFAULT_MODE,
      credentialSource: 'bitwarden',
      usernameSelector: '',
      passwordSelector: 'input[type="password"]',
      submitSelector: '',
      username: '',
      password: '',
      otpValue: '',
      steps: [
        {
          id: 'step_1',
          name: '',
          type: 'password',
          usernameSelector: '',
          passwordSelector: 'input[type="password"]',
          otpSelector: '',
          submitSelector: '',
          waitUntil: 'passwordFilled',
          autoSubmit: true,
          fillDelay: 400,
          maxAttempts: 2,
          autoDetectSubmit: true,
        },
      ],
    };
  }

  function syncPanelMarkup() {
    const settings = normalizeSyncSettings(state.syncSettings);
    const target = resolveWebDavTarget(settings);
    const statusClass = settings.enabled ? 'is-enabled' : 'is-disabled';
    const statusTime = state.syncStatus.time || formatSyncDisplayTime(settings.lastSyncAt);
    const statusMessage = state.syncStatus.message || (settings.lastError ? `上次失败:${settings.lastError}` : '尚未执行同步。');
    return `
      <form class="alh-sync-form" autocomplete="off">
        <div class="alh-sync-title">
          <div>
            <strong>WebDAV 同步</strong>
            <span class="alh-sync-state ${statusClass}">${settings.enabled ? '已启用' : '未启用'}</span>
          </div>
          <p>同一个 WebDAV JSON 可跨浏览器共用,根地址请填写 WebDAV 根路径。</p>
        </div>
        <div class="alh-sync-grid">
          <label class="alh-sync-switch"><span>启用同步</span>
            <select name="enabled">
              <option value="true" ${settings.enabled ? 'selected' : ''}>是</option>
              <option value="false" ${!settings.enabled ? 'selected' : ''}>否</option>
            </select>
          </label>
          <label class="alh-sync-switch"><span>页面加载时自动同步</span>
            <select name="autoSyncOnLoad">
              <option value="true" ${settings.autoSyncOnLoad ? 'selected' : ''}>是</option>
              <option value="false" ${!settings.autoSyncOnLoad ? 'selected' : ''}>否</option>
            </select>
          </label>
          <label class="alh-sync-switch"><span>保存配置后自动推送</span>
            <select name="autoSyncOnSave">
              <option value="true" ${settings.autoSyncOnSave ? 'selected' : ''}>是</option>
              <option value="false" ${!settings.autoSyncOnSave ? 'selected' : ''}>否</option>
            </select>
          </label>
          <label class="alh-sync-url"><span>WebDAV 根地址</span><input name="webdavUrl" type="url" placeholder="https://dav.example.com/dav/" value="${escapeHtmlAttr(settings.webdavUrl || '')}" /></label>
          <label><span>同步目录</span><input name="syncDirectory" placeholder="${DEFAULT_WEBDAV_DIR}" value="${escapeHtmlAttr(settings.syncDirectory || DEFAULT_WEBDAV_DIR)}" /></label>
          <label><span>配置文件名</span><input name="fileName" placeholder="${DEFAULT_WEBDAV_FILENAME}" value="${escapeHtmlAttr(settings.fileName || DEFAULT_WEBDAV_FILENAME)}" /></label>
          <label><span>用户名</span><input name="username" autocomplete="off" value="${escapeHtmlAttr(settings.username || '')}" /></label>
          <label><span>密码</span><input name="password" type="password" autocomplete="new-password" value="${escapeHtmlAttr(settings.password || '')}" /></label>
        </div>
        <div class="alh-sync-preview">
          <strong>实际同步文件</strong>
          <code>${escapeHtml(target.fileUrl || '未设置')}</code>
        </div>
        <div class="alh-sync-actions">
          <button class="alh-sync-save" type="submit">保存同步设置</button>
          <button class="alh-sync-now" type="button">智能同步</button>
          <button class="alh-sync-pull" type="button">从 WebDAV 拉取</button>
          <button class="alh-sync-push" type="button">推送到 WebDAV</button>
        </div>
        <div class="alh-sync-status is-${escapeHtmlAttr(state.syncStatus.level || 'idle')}">
          <strong>状态</strong>
          <span>${escapeHtml(statusMessage)}</span>
          <em>${escapeHtml(statusTime ? `时间:${statusTime}` : '')}</em>
        </div>
      </form>
    `;
  }

  function managerMarkup() {
    return `
      <div class="alh-backdrop"></div>
      <div class="alh-panel">
        <div class="alh-header">
          <div>
            <h2>自动登录管理器</h2>
            <p>统一维护各站点的登录配置和执行方式。</p>
          </div>
          <button class="alh-close" type="button">关闭</button>
        </div>
        <div class="alh-tabs"></div>
        <div class="alh-toolbar"></div>
        <div class="alh-content"></div>
      </div>
    `;
  }

  function renderManager() {
    const root = document.getElementById(PANEL_ID);
    if (!root) return;
    renderManagerTabs(root);
    const contentEl = root.querySelector('.alh-content');
    if (!contentEl) return;
    const currentMatchedId = findPreferredManagerConfigId();
    if (state.managerTab === 'configs' && state.managerFollowMatchedConfig && currentMatchedId) {
      state.managerEditingConfigId = currentMatchedId;
      state.activeConfigId = currentMatchedId;
      state.selectedConfigIds = [currentMatchedId];
    }
    const editorConfigId = resolveEditorConfigId(currentMatchedId);
    if (state.managerTab === 'configs' && editorConfigId && state.activeConfigId !== editorConfigId) {
      state.activeConfigId = editorConfigId;
    }

    if (state.managerTab === 'sync') {
      renderManagerToolbar(root, null);
      contentEl.innerHTML = `
        <div class="alh-sync-page">
          <div class="alh-sync"></div>
        </div>
      `;
      renderSyncPanel(contentEl);
      return;
    }

    contentEl.innerHTML = `
      <div class="alh-body">
        <div class="alh-list"></div>
        <div class="alh-editor"></div>
      </div>
    `;

    const listEl = contentEl.querySelector('.alh-list');
    const editorEl = contentEl.querySelector('.alh-editor');
    const orderedConfigs = getOrderedConfigs(currentMatchedId);
    listEl.innerHTML = state.configs.length
      ? orderedConfigs.map((config) => `
          <div class="alh-list-item ${config.id === editorConfigId ? 'is-active' : ''} ${config.id === currentMatchedId ? 'is-current' : ''}" data-id="${config.id}">
            <div class="alh-list-item-head">
              <input class="alh-list-checkbox" type="checkbox" data-id="${config.id}" ${state.selectedConfigIds.includes(config.id) ? 'checked' : ''} />
              <strong>${escapeHtml(config.name || '(Unnamed)')}</strong>
            </div>
            <span>${escapeHtml(config.matchType)}: ${escapeHtml(config.matchValue || '')}</span>
            ${config.id === currentMatchedId ? '<em>当前页面命中</em>' : ''}
          </div>
        `).join('')
      : '<div class="alh-empty">当前还没有任何配置。</div>';

    listEl.querySelectorAll('.alh-list-item').forEach((button) => {
      button.addEventListener('click', (event) => {
        if (event.target.closest('.alh-list-checkbox')) return;
        state.managerFollowMatchedConfig = false;
        state.managerEditingConfigId = button.dataset.id;
        state.selectedConfigIds = [button.dataset.id];
        state.activeConfigId = button.dataset.id;
        renderManager();
      });
    });
    listEl.querySelectorAll('.alh-list-checkbox').forEach((checkbox) => {
      checkbox.addEventListener('click', (event) => {
        event.stopPropagation();
      });
      checkbox.addEventListener('change', () => {
        toggleConfigSelection(checkbox.dataset.id, checkbox.checked);
      });
    });
    scrollManagerListToCurrent(listEl, currentMatchedId || editorConfigId || state.activeConfigId);

    const active = state.configs.find((item) => item.id === (editorConfigId || state.activeConfigId)) || state.configs[0] || null;
    renderManagerToolbar(root, active);
    if (!active) {
      editorEl.innerHTML = '<div class="alh-empty">请先新建一个配置开始使用。</div>';
      return;
    }

    state.activeConfigId = active.id;
    editorEl.innerHTML = editorMarkup(active);

    const form = editorEl.querySelector('form');
    applyModeVisibility(form);
    form.querySelector('[name="mode"]').addEventListener('change', () => applyModeVisibility(form));
    form.querySelector('[name="credentialSource"]').addEventListener('change', () => applyModeVisibility(form));
    form.querySelector('[name="stepType"]').addEventListener('change', () => applyModeVisibility(form));
    form.addEventListener('submit', (event) => {
      event.preventDefault();
      persistForm(form, active.id);
    });
    editorEl.querySelector('.alh-pick-user').addEventListener('click', () => {
      startElementPicker(active.id, 'usernameSelector');
    });
    editorEl.querySelector('.alh-pick-pass').addEventListener('click', () => {
      startElementPicker(active.id, 'passwordSelector');
    });
    editorEl.querySelector('.alh-pick-otp').addEventListener('click', () => {
      startElementPicker(active.id, 'otpSelector');
    });
    editorEl.querySelector('.alh-pick-submit').addEventListener('click', () => {
      startElementPicker(active.id, 'submitSelector');
    });
    bindConfigToolbarActions(root, form, active.id);
  }

  function renderManagerTabs(root) {
    const tabsEl = root.querySelector('.alh-tabs');
    if (!tabsEl) return;
    tabsEl.innerHTML = `
      <button class="alh-tab ${state.managerTab === 'configs' ? 'is-active' : ''}" data-tab="configs" type="button">站点配置</button>
      <button class="alh-tab ${state.managerTab === 'sync' ? 'is-active' : ''}" data-tab="sync" type="button">同步设置</button>
    `;
    tabsEl.querySelectorAll('.alh-tab').forEach((button) => {
      button.addEventListener('click', () => {
        state.managerTab = button.dataset.tab === 'sync' ? 'sync' : 'configs';
        renderManager();
      });
    });
  }

  function renderManagerToolbar(root, activeConfig) {
    const toolbar = root.querySelector('.alh-toolbar');
    if (!toolbar) return;
    if (state.managerTab === 'sync') {
      toolbar.innerHTML = `
        <div class="alh-toolbar-note">
          <strong>同步设置</strong>
          <span>这里单独维护 WebDAV,同步路径和认证信息不会干扰站点配置编辑。</span>
        </div>
      `;
      return;
    }
    const allSelected = areAllConfigsSelected();
    toolbar.innerHTML = `
      <button class="alh-new" type="button">新建</button>
      <button class="alh-quick-current" type="button">当前站点快速配置</button>
      <button class="alh-select-all" type="button" ${state.configs.length ? '' : 'disabled'}>${allSelected ? '取消全选' : '全选'}</button>
      <button class="alh-batch-delete" type="button" ${state.selectedConfigIds.length ? '' : 'disabled'}>批量删除</button>
      <button class="alh-save" type="button" ${activeConfig ? '' : 'disabled'}>保存</button>
      <button class="alh-test" type="button" ${activeConfig ? '' : 'disabled'}>测试</button>
      <button class="alh-delete-one" type="button" ${activeConfig ? '' : 'disabled'}>删除</button>
      <button class="alh-export" type="button">导出</button>
      <button class="alh-import" type="button">导入</button>
    `;
    toolbar.querySelector('.alh-new').addEventListener('click', () => {
      const fresh = createEmptyConfig();
      state.configs.unshift(fresh);
      state.managerFollowMatchedConfig = false;
      state.managerEditingConfigId = fresh.id;
      state.activeConfigId = fresh.id;
      state.selectedConfigIds = [fresh.id];
      renderManager();
    });
    toolbar.querySelector('.alh-quick-current').addEventListener('click', quickSetupCurrentSiteFromManager);
    toolbar.querySelector('.alh-select-all').addEventListener('click', toggleSelectAllConfigs);
    toolbar.querySelector('.alh-batch-delete').addEventListener('click', confirmBatchDeleteConfigs);
    toolbar.querySelector('.alh-export').addEventListener('click', exportConfigs);
    toolbar.querySelector('.alh-import').addEventListener('click', importConfigs);
  }

  function bindConfigToolbarActions(root, form, configId) {
    const toolbar = root.querySelector('.alh-toolbar');
    if (!toolbar || !form || !configId) return;
    const saveButton = toolbar.querySelector('.alh-save');
    const testButton = toolbar.querySelector('.alh-test');
    const deleteButton = toolbar.querySelector('.alh-delete-one');
    if (saveButton) {
      saveButton.addEventListener('click', () => {
        persistForm(form, configId);
      });
    }
    if (testButton) {
      testButton.addEventListener('click', () => {
        persistForm(form, configId, false);
        const config = state.configs.find((item) => item.id === configId);
        const normalized = config ? normalizeConfig(config) : null;
        const activeStep = normalized ? resolveActiveStep(normalized) : null;
        if (normalized && activeStep) executeStep(normalized, activeStep, true);
      });
    }
    if (deleteButton) {
      deleteButton.addEventListener('click', () => {
        const confirmed = window.confirm('确定删除当前站点配置吗?\n\n这个操作不可撤销。');
        if (!confirmed) return;
        state.configs = state.configs.filter((item) => item.id !== configId);
        state.selectedConfigIds = state.selectedConfigIds.filter((id) => id !== configId);
        state.activeConfigId = findPreferredManagerConfigId() || (state.configs[0] ? state.configs[0].id : null);
        state.managerEditingConfigId = state.activeConfigId;
        saveConfigs();
        renderManager();
      });
    }
  }

  function renderSyncPanel(root) {
    const syncEl = root.querySelector('.alh-sync');
    if (!syncEl) return;
    syncEl.innerHTML = syncPanelMarkup();
    const form = syncEl.querySelector('.alh-sync-form');
    if (!form) return;
    form.addEventListener('submit', (event) => {
      event.preventDefault();
      persistSyncSettings(form);
    });
    syncEl.querySelector('.alh-sync-now').addEventListener('click', () => {
      persistSyncSettings(form, false);
      syncWithWebDav('two-way', { manual: true });
    });
    syncEl.querySelector('.alh-sync-pull').addEventListener('click', () => {
      persistSyncSettings(form, false);
      syncWithWebDav('pull', { manual: true });
    });
    syncEl.querySelector('.alh-sync-push').addEventListener('click', () => {
      persistSyncSettings(form, false);
      syncWithWebDav('push', { manual: true });
    });
  }

  function findPreferredManagerConfigId() {
    const current = state.configs.find((item) => item.id === state.activeConfigId);
    if (current && current.enabled !== false && isMatch(current)) {
      return current.id;
    }
    const matched = state.configs.find((config) => config.enabled !== false && isMatch(config));
    return matched ? matched.id : null;
  }

  function resolveEditorConfigId(currentMatchedId) {
    if (state.managerFollowMatchedConfig && currentMatchedId) return currentMatchedId;
    if (state.managerEditingConfigId && state.configs.some((item) => item.id === state.managerEditingConfigId)) {
      return state.managerEditingConfigId;
    }
    if (state.selectedConfigIds.length === 1 && state.configs.some((item) => item.id === state.selectedConfigIds[0])) {
      return state.selectedConfigIds[0];
    }
    if (state.activeConfigId && state.configs.some((item) => item.id === state.activeConfigId)) {
      return state.activeConfigId;
    }
    if (currentMatchedId) {
      return currentMatchedId;
    }
    return state.configs[0] ? state.configs[0].id : null;
  }

  function getOrderedConfigs(currentMatchedId) {
    const configs = [...state.configs];
    if (!currentMatchedId) return configs;
    configs.sort((left, right) => {
      if (left.id === currentMatchedId) return -1;
      if (right.id === currentMatchedId) return 1;
      return 0;
    });
    return configs;
  }

  function toggleConfigSelection(configId, checked) {
    if (!configId) return;
    state.managerFollowMatchedConfig = false;
    if (checked) {
      state.selectedConfigIds = [configId];
      state.activeConfigId = configId;
      state.managerEditingConfigId = configId;
    } else {
      state.selectedConfigIds = state.selectedConfigIds.filter((id) => id !== configId);
      if (state.activeConfigId === configId) {
        const fallbackId = findPreferredManagerConfigId() || (state.configs[0] ? state.configs[0].id : null);
        state.activeConfigId = fallbackId;
        state.managerEditingConfigId = fallbackId;
      }
    }
    renderManager();
  }

  function areAllConfigsSelected() {
    return state.configs.length > 0 && state.configs.every((config) => state.selectedConfigIds.includes(config.id));
  }

  function toggleSelectAllConfigs() {
    state.managerFollowMatchedConfig = false;
    state.selectedConfigIds = areAllConfigsSelected()
      ? []
      : state.configs.map((config) => config.id);
    if (state.selectedConfigIds.length === 1) {
      state.activeConfigId = state.selectedConfigIds[0];
      state.managerEditingConfigId = state.selectedConfigIds[0];
    } else if (!state.selectedConfigIds.length) {
      const fallbackId = findPreferredManagerConfigId() || (state.configs[0] ? state.configs[0].id : null);
      state.activeConfigId = fallbackId;
      state.managerEditingConfigId = fallbackId;
    }
    renderManager();
  }

  function confirmBatchDeleteConfigs() {
    const targets = state.configs.filter((config) => state.selectedConfigIds.includes(config.id));
    if (!targets.length) return;
    const confirmed = window.confirm(`将删除 ${targets.length} 条站点配置。\n\n这个操作不可撤销,确认继续吗?`);
    if (!confirmed) return;
    performBatchDeleteConfigs(targets.map((item) => item.id));
  }

  function scrollManagerListToCurrent(listEl, targetId) {
    if (!listEl || !targetId) return;
    const item = listEl.querySelector(`.alh-list-item[data-id="${cssEscape(targetId)}"]`);
    if (!item) return;
    window.setTimeout(() => {
      item.scrollIntoView({ block: 'nearest' });
    }, 0);
  }

  function quickSetupCurrentSiteFromManager() {
    showPrompt({
      title: `当前站点快速配置:${location.host}`,
      text: '系统会自动识别输入框和提交按钮。请选择该站点执行方式。',
      actions: [
        { label: '全自动(脚本账号密码)', onClick: () => quickAddCurrentSite('auto_script') },
        { label: '自动提交(Bitwarden)', onClick: () => quickAddCurrentSite('auto_bitwarden') },
        { label: '取消', onClick: removePrompt },
      ],
    });
  }

  function performBatchDeleteConfigs(configIds) {
    const ids = Array.isArray(configIds) ? configIds : [];
    if (!ids.length) return;
    state.configs = state.configs.filter((item) => !ids.includes(item.id));
    state.selectedConfigIds = [];
    state.activeConfigId = findPreferredManagerConfigId() || (state.configs[0] ? state.configs[0].id : null);
    saveConfigsWithOptions({ scheduleSync: false });
    renderManager();
    notify(`已删除 ${ids.length} 条配置。`);
    log('info', `已批量删除 ${ids.length} 条配置。`);
  }

  function editorMarkup(config) {
    const normalized = normalizeConfig(config);
    const step = normalized.steps[0] || normalizeLegacyStep(normalized);
    return `
      <form class="alh-form" autocomplete="off" data-lpignore="true" data-1p-ignore="true">
        <label><span>名称</span><input name="name" value="${escapeHtmlAttr(normalized.name || '')}" /></label>
        <label><span>启用</span>
          <select name="enabled">
            <option value="true" ${normalized.enabled !== false ? 'selected' : ''}>是</option>
            <option value="false" ${normalized.enabled === false ? 'selected' : ''}>否</option>
          </select>
        </label>
        <label><span>匹配类型</span>
          <select name="matchType">
            <option value="host" ${normalized.matchType === 'host' ? 'selected' : ''}>域名</option>
            <option value="url" ${normalized.matchType === 'url' ? 'selected' : ''}>URL 包含</option>
            <option value="regex" ${normalized.matchType === 'regex' ? 'selected' : ''}>regex</option>
          </select>
        </label>
        <label><span>匹配值</span><input name="matchValue" value="${escapeHtmlAttr(normalized.matchValue || '')}" /></label>
        <label><span>模式</span>
          <select name="mode">
            <option value="auto" ${normalized.mode === 'auto' ? 'selected' : ''}>自动</option>
            <option value="prompt" ${normalized.mode === 'prompt' ? 'selected' : ''}>提示</option>
          </select>
        </label>
        <label class="alh-field" data-field="credentialSource"><span>凭据来源</span>
          <select name="credentialSource">
            <option value="bitwarden" ${normalized.credentialSource === 'bitwarden' ? 'selected' : ''}>Bitwarden / 手动填充后提交</option>
            <option value="script" ${normalized.credentialSource === 'script' ? 'selected' : ''}>脚本内保存</option>
          </select>
        </label>
        <label class="alh-field" data-field="stepType"><span>阶段类型</span>
          <select name="stepType">
            <option value="username" ${step.type === 'username' ? 'selected' : ''}>用户名页</option>
            <option value="credentials" ${step.type === 'credentials' ? 'selected' : ''}>用户名和密码同页</option>
            <option value="password" ${step.type === 'password' ? 'selected' : ''}>密码页</option>
            <option value="otp" ${step.type === 'otp' ? 'selected' : ''}>验证码页</option>
          </select>
        </label>
        <label class="alh-field" data-field="cfgUserSelector"><span>用户名框元素</span>
          <span class="alh-field-inline">
            <input name="cfgUserSelector" autocomplete="off" data-lpignore="true" data-1p-ignore="true" value="${escapeHtmlAttr(step.usernameSelector || '')}" />
            <button class="alh-pick-user" type="button">提取元素</button>
          </span>
        </label>
        <label class="alh-field" data-field="cfgPassSelector"><span>密码框元素</span>
          <span class="alh-field-inline">
            <input name="cfgPassSelector" autocomplete="off" data-lpignore="true" data-1p-ignore="true" value="${escapeHtmlAttr(step.passwordSelector || '')}" />
            <button class="alh-pick-pass" type="button">提取元素</button>
          </span>
        </label>
        <label class="alh-field" data-field="cfgOtpSelector"><span>验证码框元素</span>
          <span class="alh-field-inline">
            <input name="cfgOtpSelector" autocomplete="off" data-lpignore="true" data-1p-ignore="true" value="${escapeHtmlAttr(step.otpSelector || '')}" />
            <button class="alh-pick-otp" type="button">提取元素</button>
          </span>
        </label>
        <label class="alh-field" data-field="cfgSubmitSelector"><span>提交按钮元素</span>
          <span class="alh-field-inline">
            <input name="cfgSubmitSelector" autocomplete="off" data-lpignore="true" data-1p-ignore="true" value="${escapeHtmlAttr(step.submitSelector || '')}" />
            <button class="alh-pick-submit" type="button">提取元素</button>
          </span>
        </label>
        <label class="alh-field" data-field="autoSubmit"><span>自动提交</span>
          <select name="autoSubmit">
            <option value="true" ${step.autoSubmit !== false ? 'selected' : ''}>是</option>
            <option value="false" ${step.autoSubmit === false ? 'selected' : ''}>否</option>
          </select>
        </label>
        <label class="alh-field" data-field="autoDetectSubmit"><span>自动推断登录按钮</span>
          <select name="autoDetectSubmit">
            <option value="true" ${step.autoDetectSubmit !== false ? 'selected' : ''}>是</option>
            <option value="false" ${step.autoDetectSubmit === false ? 'selected' : ''}>否</option>
          </select>
        </label>
        <label class="alh-field" data-field="fillDelay"><span>提交延时(ms)</span><input name="fillDelay" type="number" min="0" value="${escapeHtmlAttr(String(step.fillDelay || 400))}" /></label>
        <label class="alh-field" data-field="maxAttempts"><span>最大执行次数</span><input name="maxAttempts" type="number" min="1" value="${escapeHtmlAttr(String(step.maxAttempts || 2))}" /></label>
        <label class="alh-field" data-field="cfgStoredUser"><span>用户名</span><input name="cfgStoredUser" type="text" autocomplete="off" data-lpignore="true" data-1p-ignore="true" spellcheck="false" value="${escapeHtmlAttr(normalized.username || '')}" /></label>
        <label class="alh-field" data-field="cfgStoredPass"><span>密码</span><input name="cfgStoredPass" type="password" autocomplete="off" data-lpignore="true" data-1p-ignore="true" spellcheck="false" value="${escapeHtmlAttr(normalized.password || '')}" /></label>
        <label class="alh-field" data-field="cfgOtpValue"><span>验证码固定值</span><input name="cfgOtpValue" type="text" autocomplete="off" data-lpignore="true" data-1p-ignore="true" spellcheck="false" value="${escapeHtmlAttr(normalized.otpValue || '')}" /></label>
      </form>
    `;
  }

  function persistForm(form, configId, rerender = true) {
    const formData = new FormData(form);
    const stepType = String(formData.get('stepType') || 'password').trim();
    const mode = normalizeMode(formData.get('mode'));
    const effectiveCredentialSource = normalizeCredentialSource(formData.get('credentialSource') || 'bitwarden');
    const steps = [normalizeStep({
      id: 'step_1',
      name: '',
      type: stepType,
      usernameSelector: String(formData.get('cfgUserSelector') || '').trim(),
      passwordSelector: String(formData.get('cfgPassSelector') || '').trim(),
      otpSelector: String(formData.get('cfgOtpSelector') || '').trim(),
      submitSelector: String(formData.get('cfgSubmitSelector') || '').trim(),
      waitUntil: inferWaitUntil({
        type: stepType,
        usernameSelector: String(formData.get('cfgUserSelector') || '').trim(),
        passwordSelector: String(formData.get('cfgPassSelector') || '').trim(),
        otpSelector: String(formData.get('cfgOtpSelector') || '').trim(),
      }),
      autoSubmit: String(formData.get('autoSubmit')) === 'true',
      autoDetectSubmit: String(formData.get('autoDetectSubmit')) !== 'false',
      fillDelay: Number(formData.get('fillDelay') || 400),
      maxAttempts: Number(formData.get('maxAttempts') || 2),
    }, 0)];
    const next = {
      id: configId,
      name: String(formData.get('name') || '').trim(),
      enabled: String(formData.get('enabled')) === 'true',
      matchType: String(formData.get('matchType') || 'host'),
      matchValue: String(formData.get('matchValue') || '').trim(),
      mode,
      credentialSource: effectiveCredentialSource,
      usernameSelector: steps[0].usernameSelector,
      passwordSelector: steps[0].passwordSelector,
      submitSelector: steps[0].submitSelector,
      username: String(formData.get('cfgStoredUser') || form.querySelector('[name="cfgStoredUser"]')?.value || ''),
      password: String(formData.get('cfgStoredPass') || form.querySelector('[name="cfgStoredPass"]')?.value || ''),
      otpValue: String(formData.get('cfgOtpValue') || form.querySelector('[name="cfgOtpValue"]')?.value || ''),
      steps,
    };

    const index = state.configs.findIndex((item) => item.id === configId);
    if (index >= 0) state.configs[index] = next;
    saveConfigs();
    notify('配置已保存。');
    log('info', `配置已保存:${next.name || next.id}`);
    if (rerender) renderManager();
  }

  function persistSyncSettings(form, rerender = true) {
    const formData = new FormData(form);
    state.syncSettings = normalizeSyncSettings({
      enabled: String(formData.get('enabled')) === 'true',
      autoSyncOnLoad: String(formData.get('autoSyncOnLoad')) !== 'false',
      autoSyncOnSave: String(formData.get('autoSyncOnSave')) !== 'false',
      webdavUrl: String(formData.get('webdavUrl') || '').trim(),
      syncDirectory: String(formData.get('syncDirectory') || DEFAULT_WEBDAV_DIR).trim() || DEFAULT_WEBDAV_DIR,
      fileName: String(formData.get('fileName') || DEFAULT_WEBDAV_FILENAME).trim() || DEFAULT_WEBDAV_FILENAME,
      username: String(formData.get('username') || ''),
      password: String(formData.get('password') || ''),
      lastAutoSyncAt: state.syncSettings.lastAutoSyncAt,
      lastSyncAt: state.syncSettings.lastSyncAt,
      lastError: state.syncSettings.lastError,
    });
    saveSyncSettings();
    state.syncStatus = {
      level: 'info',
      message: state.syncSettings.enabled ? '同步设置已保存。' : '已保存,当前处于未启用状态。',
      time: new Date().toLocaleString(),
    };
    if (rerender && document.getElementById(PANEL_ID)) {
      renderManager();
    }
  }

  async function syncWithWebDav(mode = 'two-way', options = {}) {
    if (state.syncInProgress) {
      if (options.manual) {
        state.syncStatus = {
          level: 'warn',
          message: '已有同步任务正在执行,请稍候。',
          time: new Date().toLocaleString(),
        };
        if (document.getElementById(PANEL_ID)) renderManager();
      }
      return false;
    }

    const settings = normalizeSyncSettings(state.syncSettings);
    if (!settings.enabled || !settings.webdavUrl) {
      if (options.manual) {
        state.syncStatus = {
          level: 'warn',
          message: '请先启用 WebDAV 同步并填写文件地址。',
          time: new Date().toLocaleString(),
        };
        if (document.getElementById(PANEL_ID)) renderManager();
      }
      return false;
    }

    state.syncInProgress = true;
    state.syncStatus = {
      level: 'info',
      message: `正在执行${getSyncModeLabel(mode)}...`,
      time: new Date().toLocaleString(),
    };
    if (document.getElementById(PANEL_ID)) renderManager();

    try {
      const localPayload = buildSyncPayload();

      if (mode === 'push') {
        await uploadRemotePayload(localPayload, settings);
        finalizeSyncSuccess('已将本地配置推送到 WebDAV。');
        return true;
      }

      const remotePayload = await downloadRemotePayload(settings);
      if (mode === 'pull') {
        if (!remotePayload) {
          throw new Error('WebDAV 远端文件不存在,无法拉取。');
        }
        applyRemotePayload(remotePayload);
        finalizeSyncSuccess('已从 WebDAV 拉取配置。');
        return true;
      }

      if (!remotePayload) {
        await uploadRemotePayload(localPayload, settings);
        finalizeSyncSuccess('远端暂无配置,已自动创建并推送本地配置。');
        return true;
      }

      const remoteUpdatedAt = parseTimestamp(remotePayload.updatedAt);
      const localUpdatedAt = parseTimestamp(localPayload.updatedAt);

      if (remoteUpdatedAt > localUpdatedAt) {
        applyRemotePayload(remotePayload);
        finalizeSyncSuccess('检测到 WebDAV 配置更新较新,已拉取覆盖本地。');
        return true;
      }

      await uploadRemotePayload(localPayload, settings);
      finalizeSyncSuccess(remoteUpdatedAt === localUpdatedAt
        ? '本地与远端时间戳一致,已重新推送确保内容一致。'
        : '本地配置较新,已推送到 WebDAV。');
      return true;
    } catch (error) {
      finalizeSyncFailure(error);
      return false;
    } finally {
      state.syncInProgress = false;
      if (document.getElementById(PANEL_ID)) renderManager();
    }
  }

  function buildSyncPayload() {
    return {
      version: SYNC_VERSION,
      updatedAt: state.configMeta.updatedAt || new Date().toISOString(),
      exportedAt: new Date().toISOString(),
      configs: state.configs,
    };
  }

  function applyRemotePayload(payload) {
    if (!payload || !Array.isArray(payload.configs)) {
      throw new Error('WebDAV 返回的数据格式不正确。');
    }
    state.configs = payload.configs;
    state.activeConfigId = state.configs.find((item) => item.id === state.activeConfigId)
      ? state.activeConfigId
      : (state.configs[0] ? state.configs[0].id : null);
    saveConfigsWithOptions({
      markUpdatedAt: false,
      updatedAt: normalizeIsoTimestamp(payload.updatedAt) || new Date().toISOString(),
      scheduleSync: false,
    });
    log('info', `已从 WebDAV 应用 ${state.configs.length} 条配置。`);
  }

  async function downloadRemotePayload(settings) {
    const target = resolveWebDavTarget(settings);
    const response = await webdavRequest({
      method: 'GET',
      url: target.fileUrl,
      username: settings.username,
      password: settings.password,
      headers: {
        Accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
      },
      allowNotFound: true,
    });
    if (response.status === 404 || !response.responseText) {
      return null;
    }
    try {
      return JSON.parse(response.responseText);
    } catch (error) {
      throw new Error(`远端配置 JSON 解析失败:${error.message}`);
    }
  }

  async function uploadRemotePayload(payload, settings) {
    const target = resolveWebDavTarget(settings);
    await ensureWebDavDirectory(target, settings);
    await webdavRequest({
      method: 'PUT',
      url: target.fileUrl,
      username: settings.username,
      password: settings.password,
      data: JSON.stringify(payload, null, 2),
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
    });
  }

  function finalizeSyncSuccess(message) {
    const time = new Date().toLocaleString();
    state.syncSettings.lastSyncAt = new Date().toISOString();
    state.syncSettings.lastError = '';
    saveSyncSettings();
    state.syncStatus = {
      level: 'success',
      message,
      time,
    };
    log('info', message);
  }

  function finalizeSyncFailure(error) {
    const message = error instanceof Error ? error.message : String(error);
    state.syncSettings.lastError = message;
    saveSyncSettings();
    state.syncStatus = {
      level: 'error',
      message: `同步失败:${message}`,
      time: new Date().toLocaleString(),
    };
    log('error', `WebDAV 同步失败:${message}`);
  }

  function getSyncModeLabel(mode) {
    if (mode === 'pull') return '拉取';
    if (mode === 'push') return '推送';
    return '智能同步';
  }

  function buildInitialSyncStatus(settings) {
    const normalized = normalizeSyncSettings(settings);
    if (normalized.lastError) {
      return {
        level: 'error',
        message: `上次同步失败:${normalized.lastError}`,
        time: formatSyncDisplayTime(normalized.lastSyncAt),
      };
    }
    if (normalized.lastSyncAt) {
      return {
        level: 'idle',
        message: '已保存同步设置,等待下一次同步。',
        time: formatSyncDisplayTime(normalized.lastSyncAt),
      };
    }
    return {
      level: 'idle',
      message: '尚未执行同步。',
      time: '',
    };
  }

  function parseTimestamp(value) {
    const timestamp = Date.parse(normalizeIsoTimestamp(value));
    return Number.isFinite(timestamp) ? timestamp : 0;
  }

  function normalizeIsoTimestamp(value) {
    return typeof value === 'string' && value.trim() ? value.trim() : '';
  }

  function resolveWebDavTarget(settings) {
    const rawUrl = String(settings?.webdavUrl || '').trim();
    if (!rawUrl) {
      return {
        baseUrl: '',
        directoryUrl: '',
        fileUrl: '',
      };
    }

    const normalizedBase = rawUrl.endsWith('/') ? rawUrl : `${rawUrl}/`;
    const directory = sanitizeWebDavPathSegment(String(settings?.syncDirectory || DEFAULT_WEBDAV_DIR).trim() || DEFAULT_WEBDAV_DIR);
    const fileName = sanitizeWebDavFileName(String(settings?.fileName || DEFAULT_WEBDAV_FILENAME).trim() || DEFAULT_WEBDAV_FILENAME);
    const directoryUrl = directory ? `${normalizedBase}${directory}/` : normalizedBase;
    return {
      baseUrl: normalizedBase,
      directoryUrl,
      fileUrl: `${directoryUrl}${fileName}`,
    };
  }

  async function ensureWebDavDirectory(target, settings) {
    if (!target || !target.baseUrl || !target.directoryUrl || target.directoryUrl === target.baseUrl) {
      return;
    }
    const relativePath = target.directoryUrl.slice(target.baseUrl.length).replace(/\/$/, '');
    if (!relativePath) return;
    const segments = relativePath.split('/').filter(Boolean);
    let currentUrl = target.baseUrl;
    for (const segment of segments) {
      currentUrl = `${currentUrl}${segment}/`;
      await webdavRequest({
        method: 'MKCOL',
        url: currentUrl,
        username: settings.username,
        password: settings.password,
        allowStatuses: [201, 301, 405],
      });
    }
  }

  function sanitizeWebDavPathSegment(value) {
    return String(value || '')
      .split('/')
      .map((part) => part.trim())
      .filter(Boolean)
      .join('/');
  }

  function sanitizeWebDavFileName(value) {
    const cleaned = String(value || '').trim().replace(/[\\/]/g, '');
    return cleaned || DEFAULT_WEBDAV_FILENAME;
  }

  function formatSyncDisplayTime(value) {
    const normalized = normalizeIsoTimestamp(value);
    if (!normalized) return '';
    const date = new Date(normalized);
    return Number.isNaN(date.getTime()) ? normalized : date.toLocaleString();
  }

  function webdavRequest({ method, url, username, password, headers, data, allowNotFound = false, allowStatuses = [] }) {
    return new Promise((resolve, reject) => {
      const requestHeaders = { ...(headers || {}) };
      if (username || password) {
        requestHeaders.Authorization = `Basic ${toBase64(`${username || ''}:${password || ''}`)}`;
      }
      GM_xmlhttpRequest({
        method,
        url,
        headers: requestHeaders,
        data,
        onload: (response) => {
          if ((response.status >= 200 && response.status < 300) || allowStatuses.includes(response.status) || (allowNotFound && response.status === 404)) {
            resolve(response);
            return;
          }
          const maybeDirectoryHint = response.status === 403
            ? ',请确认 WebDAV 账号有读写权限'
            : response.status === 404
              ? ',请确认 WebDAV 根地址正确;脚本会自动创建同步目录,但根地址本身必须存在'
            : '';
          reject(new Error(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}${maybeDirectoryHint}`));
        },
        onerror: () => {
          reject(new Error('网络请求失败,请检查地址、认证信息或跨域权限。'));
        },
        ontimeout: () => {
          reject(new Error('网络请求超时。'));
        },
      });
    });
  }

  function toBase64(value) {
    try {
      return window.btoa(unescape(encodeURIComponent(value)));
    } catch (error) {
      return window.btoa(value);
    }
  }

  function applyModeVisibility(form) {
    if (!form) return;
    const credentialSource = normalizeCredentialSource(form.querySelector('[name="credentialSource"]')?.value || 'bitwarden');
    const stepType = String(form.querySelector('[name="stepType"]')?.value || 'password');
    const storedFields = ['cfgStoredUser', 'cfgStoredPass', 'cfgOtpValue'];
    const autoOnlyFields = ['autoDetectSubmit', 'fillDelay', 'maxAttempts'];
    const selectorEnableMap = {
      cfgUserSelector: stepType === 'username' || stepType === 'credentials',
      cfgPassSelector: stepType === 'password' || stepType === 'credentials',
      cfgOtpSelector: stepType === 'otp',
      cfgSubmitSelector: true,
    };

    const credentialSourceField = form.querySelector('[data-field="credentialSource"]');
    setFieldHidden(credentialSourceField, false);
    setFieldDisabled(credentialSourceField, false);

    storedFields.forEach((field) => {
      const fieldEl = form.querySelector(`[data-field="${field}"]`);
      setFieldHidden(fieldEl, false);
      setFieldDisabled(fieldEl, credentialSource !== 'script');
    });

    autoOnlyFields.forEach((field) => {
      const fieldEl = form.querySelector(`[data-field="${field}"]`);
      setFieldHidden(fieldEl, false);
      setFieldDisabled(fieldEl, false);
    });

    Object.keys(selectorEnableMap).forEach((field) => {
      const fieldEl = form.querySelector(`[data-field="${field}"]`);
      setFieldHidden(fieldEl, false);
      setFieldDisabled(fieldEl, selectorEnableMap[field] !== true);
    });
  }

  function setFieldHidden(element, hidden) {
    if (!element) return;
    element.classList.toggle('is-hidden', hidden);
  }

  function setFieldDisabled(element, disabled) {
    if (!element) return;
    element.classList.toggle('is-disabled', disabled);
    element.querySelectorAll('input, select, textarea, button').forEach((control) => {
      control.disabled = disabled;
    });
  }

  function exportConfigs() {
    const payload = buildSyncPayload();
    const fileName = `auto-login-helper-export-${formatFileTimestamp(new Date())}.json`;
    downloadTextFile(fileName, JSON.stringify(payload, null, 2), 'application/json;charset=utf-8');
    state.syncStatus = {
      level: 'success',
      message: `已导出配置文件:${fileName}`,
      time: new Date().toLocaleString(),
    };
    if (document.getElementById(PANEL_ID)) renderManager();
    log('info', `已导出配置文件:${fileName}`);
  }

  function importConfigs() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'application/json,.json';
    input.addEventListener('change', () => {
      const file = input.files && input.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = () => {
        try {
          const raw = typeof reader.result === 'string' ? reader.result : '';
          const parsed = JSON.parse(raw);
          const imported = normalizeImportedConfigBundle(parsed);
          state.configs = imported.configs;
          state.activeConfigId = state.configs[0] ? state.configs[0].id : null;
          saveConfigsWithOptions({
            updatedAt: imported.updatedAt || new Date().toISOString(),
          });
          renderManager();
          notify('导入成功。');
          log('info', `已导入 ${state.configs.length} 条配置。`);
        } catch (error) {
          window.alert(`导入失败:${error.message}`);
          log('error', `导入失败:${error.message}`);
        }
      };
      reader.onerror = () => {
        window.alert('导入失败:文件读取失败。');
        log('error', '导入失败:文件读取失败。');
      };
      reader.readAsText(file, 'utf-8');
    }, { once: true });
    input.click();
  }

  function normalizeImportedConfigBundle(value) {
    if (Array.isArray(value)) {
      return {
        configs: value,
        updatedAt: new Date().toISOString(),
      };
    }
    if (!value || typeof value !== 'object') {
      throw new Error('JSON 格式不正确。');
    }
    if (Array.isArray(value.siteConfigs)) {
      return {
        configs: value.siteConfigs,
        updatedAt: normalizeIsoTimestamp(value.updatedAt) || new Date().toISOString(),
      };
    }
    if (!Array.isArray(value.configs)) {
      throw new Error('导入文件缺少 configs 数组。');
    }
    return {
      configs: value.configs,
      updatedAt: normalizeIsoTimestamp(value.updatedAt) || new Date().toISOString(),
    };
  }

  function downloadTextFile(fileName, content, mimeType) {
    const blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.setTimeout(() => {
      URL.revokeObjectURL(url);
    }, 1000);
  }

  function formatFileTimestamp(date) {
    const yyyy = date.getFullYear();
    const mm = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    const hh = String(date.getHours()).padStart(2, '0');
    const mi = String(date.getMinutes()).padStart(2, '0');
    const ss = String(date.getSeconds()).padStart(2, '0');
    return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
  }

  function showPrompt({ title, text, actions }) {
    removePrompt();
    const root = document.createElement('div');
    root.id = PROMPT_ID;
    root.innerHTML = `
      <div class="alh-prompt">
        <div class="alh-prompt-header">
          <strong>${escapeHtml(title)}</strong>
          <button class="alh-prompt-close" type="button">×</button>
        </div>
        <p>${escapeHtml(text)}</p>
        <div class="alh-prompt-actions"></div>
      </div>
    `;
    makeDraggable(root.querySelector('.alh-prompt'), root.querySelector('.alh-prompt-header'));
    root.querySelector('.alh-prompt-close').addEventListener('click', removePrompt);
    const actionsEl = root.querySelector('.alh-prompt-actions');
    actions.forEach((action) => {
      const button = document.createElement('button');
      button.type = 'button';
      button.textContent = action.label;
      button.addEventListener('click', action.onClick);
      actionsEl.appendChild(button);
    });
    document.body.appendChild(root);
  }

  function removePrompt() {
    const existing = document.getElementById(PROMPT_ID);
    if (existing) existing.remove();
  }

  function notify(message) {
    if (state.logEnabled) {
      console.info('[自动登录]', message);
      log('info', message);
    }
  }

  function renderLogPanel() {
    if (!state.logEnabled) return;
    if (!document.body) return;
    let root = document.getElementById(LOG_ID);
    if (!root) {
      root = document.createElement('div');
      root.id = LOG_ID;
      root.innerHTML = `
        <div class="alh-log-panel is-collapsed">
          <div class="alh-log-header">
            <strong>自动登录日志</strong>
            <div class="alh-log-actions">
              <button class="alh-log-toggle" type="button">展开</button>
              <button class="alh-log-close" type="button">隐藏</button>
            </div>
          </div>
          <div class="alh-log-body"></div>
        </div>
      `;
      document.body.appendChild(root);
      const logPanel = root.querySelector('.alh-log-panel');
      makeDraggable(logPanel, root.querySelector('.alh-log-header'));
      root.querySelector('.alh-log-toggle').addEventListener('click', () => {
        logPanel.classList.toggle('is-collapsed');
        root.querySelector('.alh-log-toggle').textContent = logPanel.classList.contains('is-collapsed') ? '展开' : '收起';
      });
      root.querySelector('.alh-log-close').addEventListener('click', () => {
        root.style.display = 'none';
      });
      logPanel.style.left = '18px';
      logPanel.style.bottom = '18px';
    }
    const body = root.querySelector('.alh-log-body');
    body.innerHTML = state.logs.length
      ? state.logs.slice(-50).reverse().map((entry) => `
          <div class="alh-log-item is-${escapeHtmlAttr(entry.level)}">
            <span>${escapeHtml(entry.time)}</span>
            <strong>${escapeHtml(entry.level.toUpperCase())}</strong>
            <em>${escapeHtml(entry.message)}</em>
          </div>
        `).join('')
      : '<div class="alh-log-empty">暂无日志。</div>';
  }

  function toggleLogPanel() {
    state.logEnabled = true;
    saveLogEnabled();
    renderLogPanel();
    const root = document.getElementById(LOG_ID);
    if (!root) return;
    root.style.display = 'block';
    const panel = root.querySelector('.alh-log-panel');
    if (panel) {
      panel.classList.remove('is-collapsed');
      const toggle = root.querySelector('.alh-log-toggle');
      if (toggle) toggle.textContent = '收起';
    }
  }

  function disableLogPanel() {
    state.logEnabled = false;
    saveLogEnabled();
    clearLogs(false);
    const root = document.getElementById(LOG_ID);
    if (root) root.remove();
  }

  function clearLogs(shouldRender = true) {
    state.logs = [];
    saveLogs();
    if (shouldRender && state.logEnabled) renderLogPanel();
  }

  function log(level, message) {
    if (!state.logEnabled) return;
    const entry = {
      level,
      message,
      time: new Date().toLocaleString(),
    };
    state.logs.push(entry);
    state.logs = state.logs.slice(-MAX_LOGS);
    saveLogs();
    renderLogPanel();
  }

  function startElementPicker(configId, fieldName) {
    stopElementPicker();
    notify('请选择页面上的目标元素,按 Esc 可取消。');
    log('info', `开始选择元素:${fieldName}`);
    state.picker = {
      configId,
      fieldName,
      overlay: null,
      moveHandler: null,
      clickHandler: null,
      keyHandler: null,
    };

    removeManager();
    const overlay = document.createElement('div');
    overlay.className = 'alh-picker-highlight';
    document.body.appendChild(overlay);
    state.picker.overlay = overlay;
    removePrompt();

    state.picker.moveHandler = (event) => {
      const rawTarget = event.target;
      if (!(rawTarget instanceof Element) || isInternalUiElement(rawTarget)) {
        overlay.style.display = 'none';
        return;
      }
      const target = resolvePickerTarget(rawTarget, fieldName);
      const rect = target.getBoundingClientRect();
      overlay.style.display = 'block';
      overlay.style.left = `${rect.left + window.scrollX}px`;
      overlay.style.top = `${rect.top + window.scrollY}px`;
      overlay.style.width = `${rect.width}px`;
      overlay.style.height = `${rect.height}px`;
    };

    state.picker.clickHandler = (event) => {
      const rawTarget = event.target;
      if (!(rawTarget instanceof Element)) return;
      if (isInternalUiElement(rawTarget)) return;
      event.preventDefault();
      event.stopPropagation();
      const target = resolvePickerTarget(rawTarget, fieldName);
      const selector = buildSelector(target);
      const config = state.configs.find((item) => item.id === configId);
      if (config) {
        const normalized = normalizeConfig(config);
        if (fieldName === 'usernameSelector' || fieldName === 'passwordSelector' || fieldName === 'submitSelector' || fieldName === 'otpSelector') {
          normalized.steps[0][fieldName] = selector;
          if (fieldName !== 'otpSelector') {
            normalized[fieldName] = selector;
          }
          Object.assign(config, normalized);
        } else {
          config[fieldName] = selector;
        }
        saveConfigs();
        log('info', `已为 ${fieldName} 选择元素:${selector}`);
        renderManager();
      }
      stopElementPicker();
    };

    state.picker.keyHandler = (event) => {
      if (event.key === 'Escape') {
        log('info', '已取消元素选择。');
        stopElementPicker();
      }
    };

    document.addEventListener('mousemove', state.picker.moveHandler, true);
    document.addEventListener('click', state.picker.clickHandler, true);
    document.addEventListener('keydown', state.picker.keyHandler, true);
  }

  function stopElementPicker() {
    if (!state.picker) return;
    const configId = state.picker.configId;
    document.removeEventListener('mousemove', state.picker.moveHandler, true);
    document.removeEventListener('click', state.picker.clickHandler, true);
    document.removeEventListener('keydown', state.picker.keyHandler, true);
    if (state.picker.overlay) state.picker.overlay.remove();
    state.picker = null;
    openManager(configId);
  }

  function resolvePickerTarget(target, fieldName) {
    if (!(target instanceof Element)) return target;
    if (fieldName === 'submitSelector') {
      return resolveButtonCandidate(target);
    }
    return target;
  }

  function resolveButtonCandidate(target) {
    const direct = target.closest('button, input[type="submit"], [role="button"], a.button, a.btn');
    if (direct && isVisible(direct) && !isOversized(direct)) return direct;

    const child = findLikelySubmit(target);
    if (child && !isOversized(child)) return child;

    let current = target;
    for (let index = 0; current && index < 4; index += 1) {
      if (!isOversized(current)) {
        const nested = findLikelySubmit(current);
        if (nested && !isOversized(nested)) return nested;
      }
      current = current.parentElement;
    }

    return target;
  }

  function isOversized(element) {
    if (!(element instanceof Element)) return false;
    const rect = element.getBoundingClientRect();
    return rect.width > Math.max(window.innerWidth * 0.8, 500) || rect.height > Math.max(window.innerHeight * 0.35, 220);
  }

  function describeElement(element) {
    if (!(element instanceof Element)) return 'unknown';
    const rect = element.getBoundingClientRect();
    const text = `${element.textContent || ''}`.trim().replace(/\s+/g, ' ').slice(0, 40);
    const attrs = [
      element.tagName.toLowerCase(),
      element.id ? `#${element.id}` : '',
      element.getAttribute('name') ? `[name="${element.getAttribute('name')}"]` : '',
      element.getAttribute('type') ? `[type="${element.getAttribute('type')}"]` : '',
      element.getAttribute('role') ? `[role="${element.getAttribute('role')}"]` : '',
    ].filter(Boolean).join('');
    return `${attrs} 文本="${text}" 尺寸=${Math.round(rect.width)}x${Math.round(rect.height)}`;
  }

  function makeDraggable(panel, handle) {
    if (!panel || !handle || handle.dataset.alhDragBound === 'true') return;
    handle.dataset.alhDragBound = 'true';
    handle.style.cursor = 'move';
    handle.addEventListener('mousedown', (event) => {
      if (event.target.closest('button, input, select, textarea')) return;
      const rect = panel.getBoundingClientRect();
      panel.style.margin = '0';
      panel.style.left = `${rect.left}px`;
      panel.style.top = `${rect.top}px`;
      panel.style.right = 'auto';
      panel.style.bottom = 'auto';
      state.drag = {
        panel,
        offsetX: event.clientX - rect.left,
        offsetY: event.clientY - rect.top,
      };
      event.preventDefault();
    });
  }

  document.addEventListener('mousemove', (event) => {
    if (!state.drag) return;
    const left = Math.max(0, Math.min(window.innerWidth - 120, event.clientX - state.drag.offsetX));
    const top = Math.max(0, Math.min(window.innerHeight - 60, event.clientY - state.drag.offsetY));
    state.drag.panel.style.left = `${left}px`;
    state.drag.panel.style.top = `${top}px`;
    state.drag.panel.style.position = 'fixed';
  });

  document.addEventListener('mouseup', () => {
    if (state.drag) state.drag = null;
  });

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const css = `
      #${PANEL_ID}, #${PROMPT_ID}, #${LOG_ID} {
        font-family: "Segoe UI", sans-serif;
      }
      #${PANEL_ID}, #${PROMPT_ID}, #${PANEL_ID} *, #${PROMPT_ID} * {
        box-sizing: border-box;
        font-family: "Segoe UI", sans-serif;
      }
      #${LOG_ID}, #${LOG_ID} * {
        box-sizing: border-box;
        font-family: "Segoe UI", sans-serif;
      }
      #${PANEL_ID} {
        position: fixed;
        inset: 0;
        z-index: 2147483646;
      }
      #${PANEL_ID} .alh-backdrop {
        position: absolute;
        inset: 0;
        background: rgba(10, 18, 30, 0.45);
      }
      #${PANEL_ID} .alh-panel {
        position: relative;
        display: flex;
        flex-direction: column;
        width: min(1100px, calc(100vw - 40px));
        height: min(760px, calc(100vh - 40px));
        margin: 20px auto;
        background: #f6f2ea;
        color: #1f2a30;
        border-radius: 18px;
        overflow-x: hidden;
        overflow-y: auto;
        box-shadow: 0 18px 70px rgba(0, 0, 0, 0.28);
      }
      #${PANEL_ID} .alh-header,
      #${PANEL_ID} .alh-toolbar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        padding: 16px 20px;
        background: linear-gradient(135deg, #efe3cf, #d7e4dc);
      }
      #${PANEL_ID} .alh-tabs {
        display: flex;
        gap: 10px;
        padding: 14px 20px 0;
        background: linear-gradient(135deg, #efe3cf, #d7e4dc);
        border-top: 1px solid rgba(31, 42, 48, 0.08);
      }
      #${PANEL_ID} .alh-tab {
        border: 0;
        border-radius: 14px 14px 0 0;
        padding: 12px 18px;
        background: rgba(36, 64, 74, 0.14);
        color: #24404a;
        cursor: pointer;
        font-weight: 700;
      }
      #${PANEL_ID} .alh-tab.is-active {
        background: #f6f2ea;
        color: #1f2a30;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
      }
      #${PANEL_ID} h2 {
        margin: 0 0 6px;
        font-size: 24px;
      }
      #${PANEL_ID} p {
        margin: 0;
        color: #43535b;
      }
      #${PANEL_ID} .alh-toolbar {
        justify-content: flex-start;
        flex-wrap: wrap;
        padding-top: 10px;
        padding-bottom: 10px;
        border-top: 1px solid rgba(31, 42, 48, 0.08);
      }
      #${PANEL_ID} .alh-toolbar-note {
        display: flex;
        flex-direction: column;
        gap: 4px;
        color: #33434b;
      }
      #${PANEL_ID} .alh-toolbar-note strong {
        color: #1f2a30;
      }
      #${PANEL_ID} .alh-content {
        display: flex;
        flex: 1 1 auto;
        min-height: 0;
      }
      #${PANEL_ID} .alh-sync {
        padding: 14px;
        background: #f8f4ec;
        border-radius: 12px;
        border: 1px solid rgba(31, 42, 48, 0.08);
        box-shadow: 0 6px 18px rgba(31, 42, 48, 0.05);
      }
      #${PANEL_ID} .alh-sync-page {
        flex: 1;
        padding: 12px 14px 14px;
        overflow: auto;
      }
      #${PANEL_ID} .alh-sync-form {
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
      #${PANEL_ID} .alh-sync-title {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
      }
      #${PANEL_ID} .alh-sync-title strong {
        display: block;
        font-size: 15px;
      }
      #${PANEL_ID} .alh-sync-title p {
        font-size: 12px;
        line-height: 1.4;
        max-width: 460px;
      }
      #${PANEL_ID} .alh-sync-state {
        display: inline-flex;
        align-items: center;
        margin-top: 4px;
        padding: 3px 8px;
        border-radius: 999px;
        font-size: 11px;
      }
      #${PANEL_ID} .alh-sync-state.is-enabled {
        background: rgba(36, 64, 74, 0.12);
        color: #24404a;
      }
      #${PANEL_ID} .alh-sync-state.is-disabled {
        background: rgba(157, 67, 56, 0.12);
        color: #9d4338;
      }
      #${PANEL_ID} .alh-sync-grid {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 8px 10px;
      }
      #${PANEL_ID} .alh-sync-grid label {
        display: flex;
        flex-direction: column;
        gap: 4px;
      }
      #${PANEL_ID} .alh-sync-grid label span {
        font-size: 12px;
        color: #37474f;
      }
      #${PANEL_ID} .alh-sync-grid .alh-sync-switch {
        min-width: 0;
      }
      #${PANEL_ID} .alh-sync-grid .alh-sync-url {
        grid-column: 1 / -1;
      }
      #${PANEL_ID} .alh-sync-grid input,
      #${PANEL_ID} .alh-sync-grid select {
        width: 100%;
        padding: 8px 10px;
        border-radius: 8px;
        border: 1px solid #bfd0ca;
        background: #fff;
        color: #1f2a30;
        min-height: 34px;
      }
      #${PANEL_ID} .alh-sync-preview {
        display: grid;
        grid-template-columns: 90px minmax(0, 1fr);
        gap: 8px;
        align-items: center;
        padding: 8px 10px;
        border-radius: 10px;
        background: #eef3ef;
        color: #233239;
        font-size: 12px;
      }
      #${PANEL_ID} .alh-sync-preview code {
        display: block;
        padding: 7px 9px;
        border-radius: 8px;
        background: rgba(36, 64, 74, 0.08);
        color: #24404a;
        word-break: break-all;
      }
      #${PANEL_ID} .alh-sync-actions {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 8px;
      }
      #${PANEL_ID} .alh-sync-actions button {
        width: 100%;
        padding: 8px 10px;
        border-radius: 10px;
        font-size: 13px;
      }
      #${PANEL_ID} .alh-sync-status {
        display: grid;
        grid-template-columns: 56px 1fr auto;
        gap: 8px;
        align-items: center;
        padding: 8px 10px;
        border-radius: 10px;
        background: rgba(36, 64, 74, 0.08);
        color: #24333b;
        font-size: 12px;
      }
      #${PANEL_ID} .alh-sync-status.is-error {
        background: rgba(157, 67, 56, 0.12);
        color: #7e3027;
      }
      #${PANEL_ID} .alh-sync-status.is-success {
        background: rgba(34, 107, 62, 0.14);
        color: #215236;
      }
      #${PANEL_ID} .alh-sync-status em {
        font-style: normal;
        opacity: 0.8;
      }
      #${PANEL_ID} .alh-body {
        display: grid;
        grid-template-columns: 280px 1fr;
        min-height: 360px;
        flex: 1 0 360px;
      }
      #${PANEL_ID} .alh-list {
        padding: 16px;
        border-right: 1px solid rgba(31, 42, 48, 0.12);
        overflow: auto;
        background: #fbf8f2;
        min-height: 360px;
      }
      #${PANEL_ID} .alh-editor {
        padding: 20px;
        overflow: auto;
        min-height: 360px;
      }
      #${PANEL_ID} .alh-list-item,
      #${PANEL_ID} button,
      #${PROMPT_ID} button {
        cursor: pointer;
        border: 0;
        border-radius: 12px;
        background: #24404a;
        color: #fff;
        padding: 10px 14px;
      }
      #${PANEL_ID} .alh-list-item {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        width: 100%;
        margin-bottom: 10px;
        background: #eef3ef;
        color: #213039;
        border: 1px solid rgba(73, 108, 98, 0.14);
        box-shadow: 0 4px 12px rgba(31, 42, 48, 0.04);
      }
      #${PANEL_ID} .alh-list-item-head {
        display: flex;
        align-items: center;
        gap: 10px;
        width: 100%;
      }
      #${PANEL_ID} .alh-list-checkbox {
        width: 16px;
        height: 16px;
        accent-color: #537c72;
        cursor: pointer;
        flex: 0 0 auto;
      }
      #${PANEL_ID} .alh-list-item.is-active {
        background: linear-gradient(135deg, #dfeee8, #edf5f0);
        color: #1f2f2d;
        border-color: rgba(83, 124, 114, 0.38);
        box-shadow: 0 10px 20px rgba(83, 124, 114, 0.12);
      }
      #${PANEL_ID} .alh-list-item.is-current {
        border-left: 4px solid #d58a2f;
        padding-left: 10px;
      }
      #${PANEL_ID} .alh-list-item span {
        margin-top: 4px;
        font-size: 12px;
        opacity: 0.82;
      }
      #${PANEL_ID} .alh-list-item em {
        margin-top: 8px;
        font-style: normal;
        font-size: 12px;
        font-weight: 700;
        color: #b06d12;
      }
      #${PANEL_ID} .alh-list-item.is-active em {
        color: #9b6114;
      }
      #${PANEL_ID} .alh-form {
        display: grid;
        grid-template-columns: repeat(3, minmax(210px, 250px));
        gap: 14px 18px;
        justify-content: start;
        max-width: 860px;
      }
      #${PANEL_ID} .alh-form label {
        display: flex;
        flex-direction: column;
        gap: 6px;
        color: #213039;
      }
      #${PANEL_ID} .alh-field-inline {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 74px;
        gap: 8px;
        align-items: center;
      }
      #${PANEL_ID} .alh-field-inline button {
        white-space: nowrap;
        padding: 10px 8px;
        font-size: 12px;
      }
      #${PANEL_ID} .alh-form .is-hidden {
        display: none;
      }
      #${PANEL_ID} .alh-form .is-disabled {
        opacity: 0.45;
      }
      #${PANEL_ID} .alh-form input,
      #${PANEL_ID} .alh-form select {
        width: 100%;
        padding: 10px 12px;
        border-radius: 10px;
        border: 1px solid #bfd0ca;
        background: #fff;
        color: #1f2a30;
      }
      #${PANEL_ID} .alh-field-inline input {
        min-width: 0;
      }
      #${PANEL_ID} .alh-actions {
        grid-column: 1 / -1;
        display: flex;
        flex-wrap: wrap;
        gap: 12px;
        margin-top: 8px;
      }
      #${PANEL_ID} .alh-delete {
        background: #9d4338;
      }
      #${PANEL_ID} .alh-delete-one {
        background: #9d4338;
      }
      #${PANEL_ID} .alh-empty {
        color: #5f6e73;
        padding: 16px;
      }
      #${PROMPT_ID} {
        position: fixed;
        right: 18px;
        bottom: 18px;
        z-index: 2147483647;
      }
      #${PROMPT_ID} .alh-prompt {
        position: fixed;
        width: min(360px, calc(100vw - 24px));
        padding: 16px;
        border-radius: 16px;
        background: linear-gradient(135deg, #24404a, #355e69);
        color: #fff;
        box-shadow: 0 14px 40px rgba(0, 0, 0, 0.28);
      }
      #${PROMPT_ID} .alh-prompt-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
      }
      #${PROMPT_ID} .alh-prompt-close {
        min-width: 36px;
        padding: 8px 10px;
      }
      #${PROMPT_ID} .alh-prompt p {
        margin: 8px 0 0;
        color: rgba(255, 255, 255, 0.88);
        line-height: 1.5;
      }
      #${PROMPT_ID} .alh-prompt-actions {
        display: flex;
        gap: 10px;
        margin-top: 14px;
        flex-wrap: wrap;
      }
      #${PROMPT_ID} button {
        background: #f2d394;
        color: #24333b;
        font-weight: 600;
      }
      #${LOG_ID} {
        position: fixed;
        inset: 0;
        z-index: 2147483644;
      }
      #${LOG_ID} .alh-log-panel {
        position: fixed;
        width: min(520px, calc(100vw - 24px));
        max-height: min(420px, calc(100vh - 40px));
        overflow: hidden;
        border-radius: 16px;
        background: #f7f2e8;
        color: #1f2a30;
        box-shadow: 0 14px 44px rgba(0, 0, 0, 0.24);
        border: 1px solid rgba(36, 64, 74, 0.18);
      }
      #${LOG_ID} .alh-log-panel.is-collapsed .alh-log-body {
        display: none;
      }
      #${LOG_ID} .alh-log-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        padding: 12px 14px;
        background: linear-gradient(135deg, #d7e4dc, #efe3cf);
      }
      #${LOG_ID} .alh-log-actions {
        display: flex;
        gap: 8px;
      }
      #${LOG_ID} .alh-log-actions button {
        cursor: pointer;
        border: 0;
        border-radius: 10px;
        background: #24404a;
        color: #fff;
        padding: 8px 10px;
      }
      #${LOG_ID} .alh-log-body {
        max-height: 330px;
        overflow: auto;
        padding: 12px;
      }
      #${LOG_ID} .alh-log-item {
        display: grid;
        grid-template-columns: 120px 56px 1fr;
        gap: 10px;
        padding: 8px 0;
        border-bottom: 1px solid rgba(31, 42, 48, 0.08);
        font-size: 12px;
      }
      #${LOG_ID} .alh-log-item strong {
        color: #24404a;
      }
      #${LOG_ID} .alh-log-item.is-error strong {
        color: #9d4338;
      }
      #${LOG_ID} .alh-log-item.is-warn strong {
        color: #b06d12;
      }
      #${LOG_ID} .alh-log-item em {
        font-style: normal;
        color: #33434b;
      }
      #${LOG_ID} .alh-log-empty {
        color: #607076;
        padding: 8px 0;
      }
      .alh-picker-highlight {
        position: absolute;
        z-index: 2147483647;
        border: 2px solid #e08a1e;
        background: rgba(224, 138, 30, 0.15);
        pointer-events: none;
        border-radius: 6px;
      }
      @media (max-width: 760px) {
        #${PANEL_ID} .alh-panel {
          width: calc(100vw - 12px);
          height: calc(100vh - 12px);
          margin: 6px;
          border-radius: 14px;
        }
        #${PANEL_ID} .alh-body {
          grid-template-columns: 1fr;
          min-height: 420px;
        }
        #${PANEL_ID} .alh-tabs {
          flex-wrap: wrap;
        }
        #${PANEL_ID} .alh-sync-grid {
          grid-template-columns: 1fr;
        }
        #${PANEL_ID} .alh-sync-preview,
        #${PANEL_ID} .alh-sync-actions,
        #${PANEL_ID} .alh-sync-status {
          grid-template-columns: 1fr;
        }
        #${PANEL_ID} .alh-sync-actions button {
          width: 100%;
        }
        #${PANEL_ID} .alh-sync-title,
        #${PANEL_ID} .alh-sync-status {
          display: block;
        }
        #${PANEL_ID} .alh-form {
          grid-template-columns: 1fr;
        }
        #${PANEL_ID} .alh-list,
        #${PANEL_ID} .alh-editor {
          min-height: 0;
        }
        #${LOG_ID} .alh-log-item {
          grid-template-columns: 1fr;
        }
      }
    `;

    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function escapeHtml(value) {
    return String(value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function escapeHtmlAttr(value) {
    return escapeHtml(value).replace(/`/g, '&#96;');
  }

  function debounce(fn, delay) {
    let timer = null;
    return function (...args) {
      window.clearTimeout(timer);
      timer = window.setTimeout(() => fn.apply(this, args), delay);
    };
  }
})();