11ze / 改善 JVS 开发体验

// ==UserScript==
// @name        改善 JVS 开发体验
// @namespace   https://github.com/11ze
// @match       *://*/*
// @noframes
// @icon        data:image/png;base64,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
// @grant       GM_addStyle
// @license     MIT
// @author      11ze
// @version     0.8.11
// @description 2026-08-30 修复表单设计组件名悬停消失——站点 hover 规则(.formitem:not(.clicked):hover 藏 .type-name)被 !important 覆盖,常显组件名悬停不丢
// ==/UserScript==

(function () {
  'use strict';

  // ==================== 常量配置区 ====================

  /**
   * @typedef {'逻辑设计' | '列表设计' | '表单设计' | '流程设计'} DesignType
   * @typedef {'打开' | '保存'} LogAction
   */

  const CONFIG = {
    TIMER_INTERVAL: 200,
    LOG_SAVE_DAYS: 365,
    LOG_BAR: { top: 14, right: 310, popupGap: 6 },
    ENV_LIST: [
      { ip: 'dev.', env: '开发站' },
      { ip: 'test.', env: '测试站' },
      { ip: 'gdae.', env: '正式站' },
    ],
  };

  const COLORS = {
    component: {
      data: '#FFD6E7', // 数据相关
      logic: '#D6E4FF', // 逻辑相关
      loop: '#D9F7D9', // 循环相关
      warning: '#FEF0C7', // 警告
      variable: '#EFDBFF', // 变量
    },
    design: {
      逻辑设计: 'blue',
      列表设计: 'orange',
      表单设计: 'green',
      流程设计: 'purple',
    },
  };

  /**
   * 组件类型上色配置:数组顺序即匹配优先级
   */
  const COMPONENT_TYPE_COLORS = [
    {
      types: [
        '数据模型',
        '跳过数据权限',
        '删除数据',
        '新增数据',
        '查询单条',
        '查询所有',
        '更新模型',
        '统计条数',
      ],
      color: COLORS.component.data,
    },
    {
      types: ['逻辑引擎', '逻辑应用'],
      color: COLORS.component.logic,
    },
    {
      types: ['循环容器', '对数组对象进行遍历'],
      color: COLORS.component.loop,
    },
    {
      types: ['中止程序', '提示消息'],
      color: COLORS.component.warning,
    },
    {
      types: [
        '对象变量',
        '数组变量',
        '对象数组变量',
        '固定变量',
        '对象结构',
        '公式值',
        '等变量',
        '结构示例',
      ],
      color: COLORS.component.variable,
    },
  ];

  const STORAGE_KEYS = {
    LOGS: '__11ze_JVS_LOG_LOGS_',
    APP_MODE_MAP: '__11ze_JVS_APP_MODE_MAP__',
    APP_NAME_MAP: '__11ze_JVS_APP_NAME_MAP__',
    HIGHLIGHT_APPS: '__11ze_HIGHLIGHT_APPS__',
    STARRED_FILTER: '__11ze_JVS_STARRED_FILTER__',
    APPCENTER_SIDEBAR: '__11ze_JVS_APPCENTER_SIDEBAR_COLLAPSED__',
  };

  // ==================== 状态管理 ====================

  /**
   * 全局状态对象(替代所有 window.xxx 状态变量)
   */
  const STATE = {
    tabDesignClicked: false, // 替代 window.secondTabDesignClicked11ze
    skipCopyComponentButton: false, // 替代 window.currentPageNotAddCopyComponentNameButton
    componentLibraryExpanded: false, // 替代 window.autoExpandComponentLibraryCategory11ze
  };

  /** 模式色板单一事实源:text 投影日志表格行色,buttonPlain 投影按钮整钮换色 CSS */
  const MODE_COLORS = {
    开发模式: { text: 'black' },
    测试模式: {
      text: 'green',
      buttonPlain: { color: '#67C23A', borderColor: '#B3E19D', hoverBackgroundColor: '#F0F9EB', hoverBorderColor: '#67C23A' },
    },
    正式模式: {
      text: 'red',
      buttonPlain: { color: '#F56C6C', borderColor: '#FAB6B6', hoverBackgroundColor: '#FEF0F0', hoverBorderColor: '#F56C6C' },
    },
  };

  /**
   * 应用名称的 DOM 选择器列表
   * 用于在页面中查找应用名称元素
   */
  const APP_NAME_SELECTORS = [
    // 应用左上角
    '#app > div > div > div.jvs-layout > div.jvs-left > div > div.el-menu-scrollbar.el-scrollbar > div.el-scrollbar__wrap > div > div.app-item-info.app-item-info-hide > div > span',
    // 应用左上角
    '#app > div > div > div.jvs-layout > div.jvs-left > div > div.el-menu-scrollbar.el-scrollbar > div.el-scrollbar__wrap > div > div.app-item-info > div > span',
    // 逻辑设计、列表设计、表单设计
    '#app > div > div > div.design-header-box > div.header-left',
    // 新版 JVS 列表设计、表单设计
    'div.design-header-box > div.header-left',
  ];

  // ==================== 环境检测 ====================

  /**
   * 检查是否包含 jvs-ui / edf-ui 的 link 标签
   * @param {boolean} isLogFunction - 是否为日志功能调用
   * @returns {boolean}
   */
  function isJVS(isLogFunction) {
    const links = document.getElementsByTagName('link');
    for (const link of links) {
      const matchList = ['jvs-ui', 'edf-ui'];

      if (link.href && matchList.some((match) => link.href.includes(match))) {
        let message = '%c「改善 JVS 开发体验」已检测到 JVS 环境';
        if (isLogFunction) {
          message += '(日志功能)';
        }
        console.log(message, 'color: #0099ff;');
        return true;
      }
    }
    return false;
  }

  if (!isJVS(false)) {
    return;
  }

  // ==================== 本地存储工具 ====================

  const jvsStorage = {
    get: (key, defaultValue = null) => {
      try {
        const value = localStorage.getItem(key);
        return value ? JSON.parse(value) : defaultValue;
      } catch (error) {
        console.error(`读取本地存储失败 [${key}]:`, error);
        jvsStorage.remove(key);
        return defaultValue;
      }
    },
    set: (key, value) => {
      try {
        localStorage.setItem(key, JSON.stringify(value));
      } catch (error) {
        console.error(`写入本地存储失败 [${key}]:`, error);
      }
    },
    remove: (key) => {
      try {
        localStorage.removeItem(key);
      } catch (error) {
        console.error(`删除本地存储失败 [${key}]:`, error);
      }
    },
  };

  // 清理废弃的存储键(切换应用功能已移除)
  jvsStorage.remove('__11ze_JVS_APP_ID_LIST_URL_MAP__');

  // ==================== 工具函数区 ====================

  /**
   * 工具函数集合
   */
  const Utils = {
    /**
     * 批量设置元素样式
     * @param {HTMLElement | null} el - 目标元素
     * @param {Partial<CSSStyleDeclaration>} styles - 样式对象
     */
    setStyles(el, styles) {
      if (!el || !styles) return;
      Object.assign(el.style, styles);
    },

    /**
     * 检查元素是否已标记
     * @param {HTMLElement} element - 目标元素
     * @param {string} attrName - 属性名
     * @param {string | null} [value=null] - 期望的属性值
     * @returns {boolean}
     */
    isMarked(element, attrName, value = null) {
      if (!element) return false;
      if (value === null) return element.hasAttribute(attrName);
      return element.getAttribute(attrName) === value;
    },

    /**
     * 标记元素
     * @param {HTMLElement} element - 目标元素
     * @param {string} attrName - 属性名
     * @param {string} [value='true'] - 属性值
     */
    mark(element, attrName, value = 'true') {
      element?.setAttribute(attrName, value);
    },

    /**
     * 复制文本到剪贴板
     * @param {string} text - 要复制的文本
     * @param {HTMLButtonElement | null} [button] - 触发按钮
     * @param {string} [successMessage='已复制'] - 成功提示
     * @returns {Promise<void>}
     */
    copyToClipboard(text, button, successMessage = '已复制') {
      const doCopy = () => {
        if (navigator.clipboard?.writeText) {
          return navigator.clipboard.writeText(text);
        }
        // 回退方法
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand('copy');
        document.body.removeChild(textArea);
        return Promise.resolve();
      };

      return doCopy()
        .then(() => {
          if (button) {
            const originalText = button.textContent;
            button.textContent = successMessage;
            button.disabled = true;
            setTimeout(() => {
              button.textContent = originalText;
              button.disabled = false;
            }, 500);
          }
        })
        .catch((err) => {
          console.error('复制失败:', err);
        });
    },
  };

  /**
   * @typedef {Object} ButtonOptions
   * @property {string} [text=''] - 按钮文本
   * @property {string} [className=''] - 额外的类名
   * @property {string} [id=''] - 元素 ID
   * @property {Record<string, string>} [dataset={}] - data 属性
   * @property {(event: MouseEvent) => void} [onClick] - 点击回调
   * @property {'primary' | 'default' | 'success' | 'warning' | 'danger'} [type='primary'] - 按钮类型
   * @property {'mini' | 'small' | 'medium'} [size='mini'] - 按钮尺寸
   * @property {boolean} [isHtml=false] - text 是否为 HTML 字符串
   */

  /**
   * 使容器可通过把手拖拽移动
   * @param {HTMLElement} handle - 拖拽把手元素
   * @param {HTMLElement} container - 被拖拽的容器元素
   * @param {{ minTop: number, maxRight: number, onMove?: (newTop: number, newRight: number) => void }} options - 拖拽配置(边界由调用方给定)
   */
  function makeDraggable(handle, container, options = {}) {
    const { minTop, maxRight, onMove } = options;

    handle.addEventListener('mousedown', (e) => {
      e.preventDefault();
      e.stopPropagation();
      const startX = e.clientX;
      const startY = e.clientY;
      const startTop = parseInt(container.style.top, 10) || minTop;
      const startRight = parseInt(container.style.right, 10) || maxRight;
      const containerHeight = container.offsetHeight;
      const originalTransition = container.style.transition;

      handle.style.cursor = 'move';
      container.style.transition = 'none';

      // 拖拽时禁用 iframe 防止 mouseup 事件丢失
      const iframes = document.querySelectorAll('iframe');
      iframes.forEach((iframe) => { iframe.style.pointerEvents = 'none'; });

      let moved = false;

      function onMouseMove(e) {
        e.preventDefault();
        moved = true;
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        const newRight = Math.min(Math.max(startRight - dx, 0), maxRight);
        const newTop = Math.max(startTop + dy, minTop);
        container.style.top = newTop + 'px';
        container.style.right = newRight + 'px';
        onMove?.(newTop, newRight, containerHeight);
      }

      function onMouseUp() {
        document.removeEventListener('mousemove', onMouseMove, true);
        document.removeEventListener('mouseup', onMouseUp, true);
        iframes.forEach((iframe) => { iframe.style.pointerEvents = ''; });
        handle.style.cursor = '';
        container.style.transition = originalTransition;

        if (moved) {
          const preventClick = (e) => {
            e.stopPropagation();
            e.stopImmediatePropagation();
            document.removeEventListener('click', preventClick, true);
          };
          document.addEventListener('click', preventClick, true);
        }
      }

      document.addEventListener('mousemove', onMouseMove, true);
      document.addEventListener('mouseup', onMouseUp, true);
    });
  }

  /**
   * 创建按钮元素
   * @param {ButtonOptions} options - 按钮配置
   * @returns {HTMLButtonElement}
   */
  function createButton(options = {}) {
    const {
      text = '',
      className = '',
      id = '',
      dataset = {},
      onClick = null,
      type = 'primary',
      size = 'mini',
      isHtml = false,
    } = options;

    const button = document.createElement('button');
    if (isHtml) {
      button.innerHTML = text;
    } else {
      button.textContent = text;
    }
    button.className =
      `modern-button el-button el-button--${type} el-button--${size} button-11ze ${className}`.trim();

    if (id) button.id = id;
    Object.entries(dataset).forEach(([key, value]) => button.setAttribute(key, value));
    if (onClick) button.addEventListener('click', onClick);

    return button;
  }

  /**
   * 幂等注入:在 host 内按 find 定位已注入元素,身份键相同则跳过,
   * 键异则移除重建;省略 keyAttr 为恒等键(存在即命中)
   * @param {Object} spec
   * @param {Element | Document | null} spec.host - 宿主(find 在其内查找)
   * @param {string} spec.find - 已注入元素的 selector(复用现有类名/id)
   * @param {string} [spec.keyAttr] - 身份属性名(挂在已注入元素上)
   * @param {string} [spec.key] - 期望身份值
   * @param {() => (Element | null)} spec.mount - 造并挂载新元素;返回 null 拒绝注入
   *   (键异时旧元素已移除,保持「移除后可不建」语义)
   * @returns {Element | null} 当前元素(host 缺失或 mount 拒绝时为 null)
   */
  function ensureInjected({ host, find, keyAttr, key, mount }) {
    if (!host) {
      return null;
    }
    const existing = host.querySelector(find);
    if (existing) {
      if (!keyAttr || existing.getAttribute(keyAttr) === key) {
        return existing;
      }
      existing.remove();
    }
    return mount() || null;
  }

  // ==================== 主逻辑 ====================

  /**
   * 创建轮询调度器,统一处理 operations 混合数组
   * 普通函数每 tick 执行;{ name, probe, apply } 对象由 probe 返回的键控制:
   * probe 返回 null/undefined 表示本 tick 不适用;键不变时跳过 apply。
   * probe 也可返回 { key, payload } 对象——键对比走 key,payload 直达
   * apply(payload)(probe 纯读,apply 消费 probe 采到的数据,不二次读取)
   * @param {Array<Function | {name: string, probe: Function, apply: Function}>} operations
   * @param {(name: string, error: unknown) => void} onError - 单个操作出错时的回调
   * @returns {() => void} tick 函数
   */
  function createOperationRunner(operations, onError) {
    const lastKeys = new Map();

    return function tick() {
      for (const operation of operations) {
        try {
          if (typeof operation === 'function') {
            operation();
            continue;
          }

          const result = operation.probe();
          if (result === null || result === undefined) continue;
          const key = result.key ?? result;
          if (lastKeys.get(operation) === key) continue;
          lastKeys.set(operation, key);
          operation.apply(result.payload);
        } catch (error) {
          onError(operation.name, error);
        }
      }
    };
  }

  // 测试钩子:浏览器中 __JVS_TEST__ 不存在,此分支永不执行
  if (window.__JVS_TEST__) {
    window.__JVS_TEST__.hooks = {
      createOperationRunner: createOperationRunner,
      paintComponents: paintComponents,
      cutOverdueLogs: cutOverdueLogs,
      uniqueLogs: uniqueLogs,
      enrichLogsWithAppName: enrichLogsWithAppName,
      saveAppIdName: saveAppIdName,
      getAppIdName: getAppIdName,
      saveLog: saveLog,
      getLogs: getLogs,
      latestLogWhere: latestLogWhere,
      getUrlFromLogs: getUrlFromLogs,
      getUrlFromLogsAndUrl: getUrlFromLogsAndUrl,
      findDesignNameById: findDesignNameById,
      getTabType: getTabType,
      getModeColor: getModeColor,
      collectAppMode: collectAppMode,
      getModeFromHistory: getModeFromHistory,
      currentMode: currentMode,
      buildModeColorCss: buildModeColorCss,
      MODE_COLORS: MODE_COLORS,
      highlightApps: highlightApps,
      filterStarredApps: filterStarredApps,
      toggleAppCenterSidebar: toggleAppCenterSidebar,
      syncAppCenterUrl: syncAppCenterUrl,
      getLogButtonName: getLogButtonName,
      ensureInjected: ensureInjected,
      addButtonToCopyDesignName: addButtonToCopyDesignName,
      addButtonToCopyComponentName: addButtonToCopyComponentName,
      addButtonToClearAllFields: addButtonToClearAllFields,
      addButtonToOpenNewLogicDesign: addButtonToOpenNewLogicDesign,
      addButtonToOpenNewFormOrListDesign: addButtonToOpenNewFormOrListDesign,
      showNodeExecTime: showNodeExecTime,
      _createCopyNameButton: _createCopyNameButton,
      _createLogicNameDisplay: _createLogicNameDisplay,
      _createOpenLogicButton: _createOpenLogicButton,
      setCanvasScroll: setCanvasScroll,
      get canvasScrollOperation() {
        return canvasScrollOperation;
      },
      get updateLogButtonOperation() {
        return updateLogButtonOperation;
      },
      getStyles: () => JVS_STYLES + buildModeColorCss(),
    };
  }

  /**
   * 构建待保存的日志条目,appName 过长或与设计名相同时回退为存储中的应用名
   * @returns {LogEntry | null} 数据不完整时返回 null
   */
  function buildLogForSave() {
    const newLog = createLogEntry();
    if (!newLog || !newLog.tabType) {
      return null;
    }
    if (newLog.appName.length > 100 || newLog.appName === newLog.designName) {
      newLog.appName = getAppIdName(newLog.jvsAppId);
    }
    return newLog;
  }

  /**
   * 保存当前页面日志(designName 或 appName 变化时落库,由调度器的键对比控制)
   * payload 携带 probe 构建好的日志条目,apply 直存不再二次构建
   */
  const saveCurrentLogOperation = {
    name: 'saveCurrentLog',
    probe() {
      const newLog = buildLogForSave();
      // \u0000 不会出现在页面名称里,用作组合键分隔符
      return newLog
        ? { key: newLog.designName + '\u0000' + newLog.appName, payload: newLog }
        : null;
    },
    apply(newLog) {
      if (newLog) {
        saveLog(newLog, '打开');
      }
    },
  };

  /**
   * 更新日志按钮(容器缺失或按钮模式与当前不符时重建,由调度器的键对比控制)
   * payload 携带 probe 读到的模式,apply 不再重取
   */
  const updateLogButtonOperation = {
    name: 'updateLogButton',
    probe() {
      const mode = currentMode();
      const existContainer = document.getElementById('ze-jvs-log-container');
      if (!existContainer) {
        return { key: 'missing', payload: mode };
      }
      const buttonMode = existContainer.querySelector('#ze-jvs-log-button')?.dataset.mode || '';
      return buttonMode === mode
        ? { key: 'stable:' + mode, payload: mode }
        : { key: 'stale:' + buttonMode + '->' + mode, payload: mode };
    },
    apply: updateLogButton,
  };

  /**
   * 滚轮平移挂载(画布容器就绪且未挂时挂载,由调度器的键对比控制)
   * 键:mount(需挂)/ restore(同一画布重建且销毁前有非零平移,挂载并带回平移)/
   * mounted(已挂)——主/循环画布切换、连线等操作会整体重建容器与画布实例,
   * 闩锁随 DOM 销毁,键回 mount/restore 触发重挂;切换画布身份变了,不重放旧画布平移
   */
  const canvasScrollOperation = {
    name: 'canvasScroll',
    probe() {
      const stage = getButterflyCanvas();
      if (!stage) {
        return null;
      }
      const identity = getCanvasIdentity();
      if (stage.canvas !== canvasScrollState.seen) {
        if (canvasScrollState.identity !== identity) {
          // 切换画布:旧画布的平移记录作废
          canvasScrollState.offset = null;
        }
        const pending = canvasScrollRestoreOf(stage.canvas) ? 'restore' : 'mount';
        // 待挂键带递增代数:两次重建之间没有 tick 时(快速连续切换),
        // 相同的裸 'mount' 会让调度器误判状态没变而永远跳过挂载(监听丢失死锁)
        return pending + '@' + (++canvasScrollState.rebuildCount);
      }
      canvasScrollState.identity = identity;
      canvasScrollState.offset = stage.canvas.getOffset();
      return stage.container.getAttribute('data-11ze-canvas-scroll') ? 'mounted' : 'mount';
    },
    apply: setCanvasScroll,
  };

  const operations = [
    // 设计器模块
    changeTitle,
    enterAppCenter,
    syncAppCenterUrl,
    enterTabDesign,
    adjustInterfaceAndComponentStyle,
    addButtonToOpenNewLogicDesign,
    firstAddButtonToOpenNewLogicDesignForNestedLogic,
    addButtonToCopyDesignName,
    addButtonToCopyComponentName,
    addButtonToClearAllFields,
    addButtonToOpenNewFormOrListDesign,
    highlightApps,
    filterStarredApps,
    toggleAppCenterSidebar,
    expandFormDesignAllComponentSettings,
    autoExpandComponentLibraryCategory,
    applicationSetClick,
    showNodeExecTime,
    canvasScrollOperation,
    // 日志模块
    updateLogButtonOperation,
    saveCurrentLogOperation,
    bindSaveButton,
  ];

  function reportOperationError(name, error) {
    console.error('「改善 JVS 开发体验」' + name + ' 运行错误:');
    console.error(error);
  }

  const jvsTimer = setInterval(
    createOperationRunner(operations, reportOperationError),
    CONFIG.TIMER_INTERVAL,
  );

  window.addEventListener('beforeunload', () => {
    clearInterval(jvsTimer);
  });

  /**
   * @typedef {Object} DesignConfig
   * @property {string} shortname - 设计类型简称
   * @property {string} color - 设计类型颜色
   */

  /**
   * @type {Record<DesignType, DesignConfig>}
   */
  const DESIGN_CONFIG = {
    逻辑设计: { shortname: '逻辑', color: COLORS.design.逻辑设计 },
    列表设计: { shortname: '列表', color: COLORS.design.列表设计 },
    表单设计: { shortname: '表单', color: COLORS.design.表单设计 },
    流程设计: { shortname: '流程', color: COLORS.design.流程设计 },
  };

  // ==================== 日志模块 ====================

  /**
   * 创建日志条目
   * @returns {LogEntry | null} 日志对象,如果数据不完整则返回 null
   */
  function createLogEntry() {
    const url = getUrl();
    const id = getQueryParamMapping(url)['id'];
    const jvsAppId = getJvsAppId();
    const tabType = getTabType();
    const designName = getNewTabTitle();
    const appName = collectAppName();

    if (!designName || !appName || !id || !jvsAppId || !tabType) {
      return null;
    }

    return { tabType, url, time: Date.now(), designName, appName, id, jvsAppId };
  }

  /**
   * 采集应用名称(用于日志记录):DOM 抓取,命中干净的单行名时
   * 顺手落库 saveAppIdName(写一次语义,应用改名不回写)
   * @returns {string} 应用名称
   */
  function collectAppName() {
    for (const selector of APP_NAME_SELECTORS) {
      const elements = document.querySelectorAll(selector);
      for (const el of elements) {
        const text = el.textContent;
        if (text) {
          const textArray = text
            .trim()
            .split('\n')
            .map((s) => s.trim())
            .filter(Boolean);
          if (textArray.length === 1) {
            const appName = textArray[0].split(' ')[0].trim();
            saveAppIdName(getJvsAppId(), appName);
            return appName;
          }
          if (document.querySelector('.list-item')) {
            return textArray[textArray.length - 3] || textArray[0];
          }
          return textArray[0];
        }
      }
    }
    return '';
  }

  /**
   * 剪切过期日志
   */
  function cutOverdueLogs(logs, currentTime) {
    for (let i = logs.length - 1; i >= 0; i--) {
      const log = logs[i];
      if (!log.time) {
        logs.splice(i, 1);
        continue;
      }
      if (Math.abs(currentTime - log.time) > CONFIG.LOG_SAVE_DAYS * 24 * 60 * 60 * 1000) {
        logs.splice(i, 1);
      }
    }
    return uniqueLogs(logs);
  }

  /**
   * 获取日志列表(纯读:剪切过期与重复条目,不改动 appName)
   */
  function getLogs() {
    const logs = jvsStorage.get(STORAGE_KEYS.LOGS, []);
    if (!logs) return [];

    return cutOverdueLogs(logs, Date.now());
  }

  /**
   * 用应用目录补全日志的 appName(仅用于展示,不回写存储)
   */
  function enrichLogsWithAppName(logs) {
    logs.forEach((log) => {
      const appName = getAppIdName(log.jvsAppId);
      if (appName) log.appName = appName;
    });
    return logs;
  }

  /**
   * 保存日志
   */
  function saveLog(logObj, type) {
    if (!logObj) return;
    logObj.type = type;
    const logList = getLogs();
    logList.push(logObj);
    jvsStorage.set(STORAGE_KEYS.LOGS, logList);
  }

  /**
   * 日志去重
   */
  function uniqueLogs(logs) {
    const seen = new Set();
    const result = [];
    for (let i = logs.length - 1; i >= 0; i--) {
      const log = logs[i];
      const key = getQueryParamMapping(log.url)['id'] + log.type;
      if (!seen.has(key)) {
        seen.add(key);
        result.push(log);
      }
    }
    return result.reverse();
  }

  /**
   * 格式化时间
   */
  function formatTime(time) {
    const date = new Date(time);
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    return `${month}-${day} ${hours}:${minutes}:${seconds}`;
  }

  /**
   * 显示日志弹窗
   */
  function showLogPopup() {
    const popupId = '11ze-jvs-log-popup';
    const oldPopup = document.getElementById(popupId);
    if (oldPopup) {
      oldPopup.remove();
      return;
    }

    const logs = enrichLogsWithAppName(getLogs());
    const appModeMap = getAppModelMap();
    const hasMode = Object.keys(appModeMap).length > 0;
    const listContent = [];

    for (let i = logs.length - 1; i >= 0; i--) {
      const oneLog = logs[i];
      if (oneLog.type === '保存') {
        continue;
      }
      const datetime = formatTime(oneLog.time);
      const currentType = DESIGN_CONFIG[oneLog.tabType] ?? { color: 'red', shortname: '未知' };

      let appName = oneLog.appName;
      if (appName.length > 16) appName = appName.substring(0, 16) + '…';

      const mode = appModeMap[oneLog.jvsAppId] ?? '';
      const modeColor = getModeColor(mode);

      listContent.push(`
        <tr class="log-11ze-table-tr">
          <td> ${datetime} &nbsp; </td>
          ${hasMode ? `<td style="color: ${modeColor}"> ${mode.replace('模式', '')} &nbsp; </td>` : ''}
          <td> ${appName} &nbsp; </td>
          <td style="color: ${currentType.color}"> ${currentType.shortname} &nbsp; </td>
          <td class="log-design-name-11ze" data-url="${oneLog.url}" style="color: #1f6fff;"> ${oneLog.designName} &nbsp; </td>
        </tr>
      `);
    }

    const popup = document.createElement('div');
    popup.className = 'popup-11ze';
    popup.id = popupId;

    // 弹窗位置以按钮右下角为准
    const btnContainer = document.getElementById('ze-jvs-log-container');
    if (btnContainer) {
      const containerTop = parseInt(btnContainer.style.top, 10) || CONFIG.LOG_BAR.top;
      popup.style.top = (containerTop + btnContainer.offsetHeight + CONFIG.LOG_BAR.popupGap) + 'px';
      popup.style.right = btnContainer.style.right || CONFIG.LOG_BAR.right + 'px';
    }

    const logTable = document.createElement('table');
    logTable.className = 'table-11ze';
    logTable.style.marginTop = '10px';
    logTable.innerHTML = `
      <thead>
        <tr style="background-color: #eef5fe" class="log-11ze-table-tr">
          <th> 时间 &nbsp;</th>
          ${hasMode ? `<th> 模式 &nbsp;</th>` : ''}
          <th> 应用 &nbsp;</th>
          <th style="text-align: center"> 类型 &nbsp;</th>
          <th> 名称 &nbsp;</th>
        </tr>
      </thead>
      <tbody>${listContent.join('')}</tbody>
    `;

    popup.appendChild(logTable);
    document.body.appendChild(popup);

    // 事件委托
    logTable.addEventListener('click', (e) => {
      const nameTd = e.target.closest('.log-design-name-11ze');
      if (nameTd) window.open(nameTd.dataset.url, '_blank');
    });

    logTable.addEventListener('auxclick', (e) => {
      if (e.button !== 1) return;
      const nameTd = e.target.closest('.log-design-name-11ze');
      if (nameTd) window.open(nameTd.dataset.url, '_blank');
    });

    document.addEventListener('click', function closePopupOnOutsideClick(event) {
      if (popup && !popup.contains(event.target) && !event.target.closest('#ze-jvs-log-container')) {
        popup.remove();
        document.removeEventListener('click', closePopupOnOutsideClick);
      }
    });
  }

  /**
   * 日志按钮文案:模式前缀与「日志」同字号同色(整钮颜色由
   * JVS_STYLES 按 data-mode 换色),间距替代全角「|」
   * @param {string} mode 当前模式名,空串表示未识别到模式
   */
  function getLogButtonName(mode) {
    return mode
      ? `<span style="margin-right: 6px">${mode}</span>日志`
      : '日志';
  }

  // 拖拽把手十字箭头,stroke 走 currentColor 跟随按钮字色
  const MOVE_SVG =
    '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/></svg>';

  function updateLogButton(mode) {
    const existContainer = document.getElementById('ze-jvs-log-container');

    if (existContainer) {
      const existButton = existContainer.querySelector('#ze-jvs-log-button');
      const buttonMode = existButton?.dataset.mode || '';
      if (buttonMode === mode) return;
      existContainer.remove();
    }

    const buttonName = getLogButtonName(mode);

    const button = createButton({
      text: buttonName,
      id: 'ze-jvs-log-button',
      isHtml: true,
      dataset: { 'data-mode': mode || '' },
    });

    Utils.setStyles(button, {
      fontSize: '14px',
    });

    button.addEventListener('click', (event) => {
      event.stopPropagation();
      showLogPopup();
    });

    const handle = document.createElement('div');
    handle.className = 'drag-handle-11ze';
    handle.innerHTML = MOVE_SVG;

    const container = document.createElement('div');
    container.id = 'ze-jvs-log-container';
    Utils.setStyles(container, {
      position: 'fixed',
      top: CONFIG.LOG_BAR.top + 'px',
      right: CONFIG.LOG_BAR.right + 'px',
      zIndex: '9998',
      display: 'flex',
      alignItems: 'stretch',
      transition: 'transform 0.2s ease',
    });
    container.appendChild(button);
    container.appendChild(handle);

    makeDraggable(handle, container, {
      minTop: CONFIG.LOG_BAR.top,
      maxRight: CONFIG.LOG_BAR.right,
      onMove(newTop, newRight, containerHeight) {
        const popup = document.getElementById('11ze-jvs-log-popup');
        if (popup) {
          popup.style.top = (newTop + containerHeight + CONFIG.LOG_BAR.popupGap) + 'px';
          popup.style.right = newRight + 'px';
        }
      },
    });
    document.body.appendChild(container);
  }

  /**
   * 绑定保存按钮点击事件
   */
  function bindSaveButton() {
    const attrName = 'log-save-bound-11ze';
    const saveButton = document.querySelector(
      '#app > div > div > div.design-header-box > div.header-right > button',
    );
    if (!saveButton || Utils.isMarked(saveButton, attrName)) return;

    Utils.mark(saveButton, attrName);
    saveButton.addEventListener('click', () => {
      const newLog = createLogEntry();
      if (newLog) saveLog(newLog, '保存');
    });
  }

  function getQueryParamMapping(url) {
    if (typeof url !== 'string' || !url) {
      return {};
    }

    // 先根据 # 分割,再平铺成基于 ? 分割的一维数组
    const urlParts = url.split('#');
    const urlQuery = urlParts.map((part) => part.split('?')[1]).join('&');

    if (!urlQuery) {
      return {};
    }

    const params = urlQuery.split('&');

    const mapping = {};

    for (let i = 0; i < params.length; i++) {
      const param = params[i];
      const id = param.split('=')[0];
      const value = param.split('=')[1];
      mapping[id] = value;
    }

    return mapping;
  }

  function getUrl() {
    return location.href;
  }

  function getJvsAppId() {
    const urlParams = getQueryParamMapping(getUrl());
    return urlParams['jvsAppId'];
  }

  /**
   * 获取当前设计器类型
   *
   * 旧版 JVS 的非设计页面上 #tab-design 页签会残留(文本「逻辑设计」),
   * 必须同时确认设计器头部存在、且文本是已知设计类型,否则视为非设计页面
   * @returns {DesignType | ''}
   */
  function getTabType() {
    if (!document.querySelector('.design-header-box')) {
      return '';
    }

    const typeDom = document.querySelector('#tab-design > span');
    if (!typeDom) {
      return '';
    }

    const tabType = typeDom.textContent.trim();
    return DESIGN_CONFIG[tabType] ? tabType : '';
  }

  function getAppModelMap() {
    return jvsStorage.get(STORAGE_KEYS.APP_MODE_MAP, {});
  }

  /**
   * 采集当前模式:读 DOM 模式项,值变才写入目录(显式的写入口)
   * @returns {string} 模式名,无 .system-list 时空串
   */
  function collectAppMode() {
    const systemList = document.querySelector('.system-list');
    if (!systemList) {
      return '';
    }

    const systemListItems = systemList.querySelectorAll('li');
    for (const systemListItem of systemListItems) {
      if (systemListItem.innerText.includes('模式')) {
        const mode = systemListItem.innerText.trim();

        const jvsAppId = getJvsAppId();
        if (jvsAppId) {
          const appModeMap = getAppModelMap();
          if (appModeMap[jvsAppId] !== mode) {
            appModeMap[jvsAppId] = mode;
            jvsStorage.set(STORAGE_KEYS.APP_MODE_MAP, appModeMap);
          }
        }

        return mode;
      }
    }

    return '';
  }

  function getModeFromHistory() {
    const urlParams = getQueryParamMapping(getUrl());
    if (!urlParams) {
      return '';
    }

    const jvsAppId = urlParams['jvsAppId'];
    if (!jvsAppId) {
      return '';
    }

    const appModeMap = getAppModelMap();
    return appModeMap[jvsAppId];
  }

  /** 当前模式唯一入口:历史目录优先,未命中走 DOM 采集 */
  function currentMode() {
    return getModeFromHistory() || collectAppMode();
  }

  function getModeColor(mode) {
    return MODE_COLORS[mode]?.text ?? 'black';
  }

  /** 按钮整钮随 data-mode 换色的规则,色值全部取自 MODE_COLORS.buttonPlain */
  function buildModeColorCss() {
    return Object.entries(MODE_COLORS)
      .filter(([, palette]) => palette.buttonPlain)
      .map(([mode, { buttonPlain }]) => `
  .button-11ze[data-mode="${mode}"] {
    color: ${buttonPlain.color} !important;
    border-color: ${buttonPlain.borderColor} !important;
  }

  .button-11ze[data-mode="${mode}"]:hover {
    background-color: ${buttonPlain.hoverBackgroundColor} !important;
    border-color: ${buttonPlain.hoverBorderColor} !important;
  }
`)
      .join('');
  }

  /**
   * 获取应用名称或设计名称,用于设置浏览器标签页 title
   * @returns {string}
   */
  function getNewTabTitle() {
    // 逻辑设计的设计名称 selector,取值函数覆盖不到,单独查询
    const title = document.querySelector(
      '#app > div > div > div.design-header-box > div.header-left > span:nth-child(3)',
    );

    if (title) {
      return title.textContent.trim();
    }

    for (let i = 1; i < APP_NAME_SELECTORS.length; i++) {
      const allTextElements = document.querySelectorAll(APP_NAME_SELECTORS[i]);

      for (let j = 0; j < allTextElements.length; j++) {
        const text = allTextElements[j].innerHTML;

        if (!text.includes('<')) {
          return text.trim();
        }

        if (i === 3) {
          let splitText = text.split('</svg>')[1];
          if (!splitText) {
            continue;
          }

          splitText = splitText.split('center;">')[1];
          if (!splitText) {
            continue;
          }

          splitText = splitText.split('<')[0];
          return splitText.trim();
        }

        const splitText = text.split('</svg>')[1];
        if (!splitText) {
          continue;
        }

        if (splitText.includes('<')) {
          return splitText.split('<')[0].trim();
        }

        return splitText.trim();
      }
    }

    if (location.href.includes('doc.html')) {
      return '接口文档';
    }

    return '';
  }

  /**
   * 修改浏览器标签页标题
   */
  function changeTitle() {
    function getEnvironment() {
      const url = location.href;

      for (let i = 0; i < CONFIG.ENV_LIST.length; i++) {
        if (url.includes(CONFIG.ENV_LIST[i].ip)) {
          return CONFIG.ENV_LIST[i].env;
        }
      }
      return '';
    }

    const newTabTitle = getNewTabTitle();
    const tabType = getTabType();

    function changeFavicon(iconURL) {
      const links = document.querySelectorAll("link[rel*='icon']");

      links.forEach(function (link) {
        link.href = iconURL;
      });
    }

    if (tabType) {
      document.title = newTabTitle;
      changeFavicon(ICONS[tabType]);
    } else {
      let prefix = collectAppMode();

      if (!prefix) {
        prefix = getEnvironment();
      }

      document.title = prefix + '|' + (newTabTitle ? newTabTitle : '未打开应用');
    }
  }

  /** 平台首页路由:enterAppCenter 靠它判定首页,syncAppCenterUrl 靠它回写地址栏 */
  const HOME_ROUTE = '#/wel/index';

  /**
   * 首次进入平台首页时自动进入应用中心
   */
  function enterAppCenter() {
    const selector =
      '#app > div > div > div.jvs-tags > div > div > div.top-nav > ul > li:nth-child(2) > span';
    const element = document.querySelector(selector);
    const url = location.href;
    if (element) {
      if (
        element.innerText === '应用中心' &&
        url.includes(HOME_ROUTE) &&
        !element.hasAttribute('app-center-clicked-11ze')
      ) {
        element.click();
        element.setAttribute('app-center-clicked-11ze', 'true');
      }
    }
  }

  /**
   * 新版应用中心不写 hash,地址栏停留在进站前路由;
   * 显示应用中心时统一替换成首页路由,刷新后 enterAppCenter 自动点击才能衔接上。
   * replaceState 不触发 hashchange,页面不动;旧版容器不同,天然不处理
   */
  function syncAppCenterUrl() {
    if (!location.hash.includes(HOME_ROUTE) && document.querySelector('.app-page')) {
      history.replaceState(null, '', HOME_ROUTE);
    }
  }

  /**
   * 在设计页面自动点击 tab,如【表单设计】
   */
  function enterTabDesign() {
    const selector = '#tab-design';

    const element = document.querySelector(selector);
    if (!element) {
      return;
    }

    // 双闩锁是序列两击计数器,缺一不可:属性闩锁 = 第一击(元素级,SPA 换页随 DOM 销毁),
    // STATE.tabDesignClicked = 第二击(会话级)——「会话内首个设计页点两次、后续设计页点一次」是有意行为
    if (element.getAttribute('second-tab-design-clicked-11ze')) {
      if (STATE.tabDesignClicked) {
        return;
      }
      STATE.tabDesignClicked = true;
      element.click();

      return;
    }

    element.click();
    element.setAttribute('second-tab-design-clicked-11ze', 'true');
  }

  /**
   * 调整界面、组件样式
   */
  function adjustInterfaceAndComponentStyle() {
    // 旧版 JVS,逻辑设计,所有在用可拖拽组件,改颜色
    paintComponents('div.jvs-rule-node.ef-node-container.jtk-droppable', 'innerText');

    // 新版 JVS,逻辑设计,所有在用可拖拽组件,改颜色
    paintComponents('.jvs-rule-node.ef-node-container', 'textContent');

    // 设置侧边栏可选组件的颜色
    paintComponents('.getItem', 'innerText');

    // 新版 JVS,表单设计,组件的名称全部显示出来
    const formComponents = document.querySelectorAll('.formitem2');
    for (const component of formComponents) {
      const parent = component.parentElement;
      if (!parent.getAttribute('draggable')) {
        continue;
      }

      component.classList.add('active-formitem2');
    }
  }

  /**
   * 给选择器命中的组件按类型上色(背景 + 边框)
   * @param {string} selector - 组件选择器
   * @param {'innerText' | 'textContent'} textProperty - 参与匹配的文本属性
   */
  function paintComponents(selector, textProperty) {
    const components = document.querySelectorAll(selector);
    for (const component of components) {
      const text = component[textProperty].trim();
      for (const typeToColor of COMPONENT_TYPE_COLORS) {
        if (typeToColor.types.some((t) => text.includes(t))) {
          component.style.backgroundColor = typeToColor.color;
          component.style.borderColor = typeToColor.color;
          component.classList.add('ze-typed');
          component.style.setProperty('--ze-type-color', typeToColor.color);
          break;
        }
      }
    }
  }

  /**
   * 倒序取首条命中 matcher 的日志(同 id 可能因 type 不同多条并存,倒序即最新优先)
   * @param {(log: object) => boolean} matcher
   * @returns {object | null}
   */
  function latestLogWhere(matcher) {
    const logs = getLogs();
    for (let i = logs.length - 1; i >= 0; i--) {
      if (matcher(logs[i])) {
        return logs[i];
      }
    }
    return null;
  }

  /**
   * 从日志或 url 生成跳转链接
   * @param {string} id - 设计 id
   * @param {boolean} isFromUrl - 是否是从 url 中获取
   * @returns {string | null}
   */
  function getUrlFromLogs(id, isFromUrl) {
    if (!id) {
      return null;
    }

    const log = latestLogWhere((item) => item.id === id);
    if (log) {
      return log.url;
    }

    if (!isFromUrl) {
      return null;
    }

    // http://xxx?id=xxx&xxx
    const url = location.href;
    return url.replace(/id=([^&]*)/, `id=${id}`);
  }

  /**
   * 从日志和 url 生成跳转链接
   * @param {string} logicName - 设计名称
   * @param {string} jvsAppId - 应用 id
   * @returns {string | null}
   */
  function getUrlFromLogsAndUrl(logicName, jvsAppId) {
    if (!logicName) {
      return null;
    }

    const log = latestLogWhere(
      (item) => item.jvsAppId === jvsAppId && item.designName === logicName
    );
    return log ? log.url : null;
  }

  /**
   * 按设计 id 反查设计名(查看逻辑按钮的展示与复制名用)
   * @param {string} id - 设计 id
   * @returns {string | null}
   */
  function findDesignNameById(id) {
    const log = latestLogWhere((item) => item.id === id);
    return log ? log.designName : null;
  }

  /**
   * 逻辑设计,检查到【逻辑调用】组件时,自动添加一个按钮用于查看对应的逻辑设计
   */
  function addButtonToOpenNewLogicDesign() {
    const labels = document.querySelectorAll('.el-form-item__label');
    for (const label of labels) {
      if (!label.innerText.includes('逻辑引擎远程调用')) {
        continue;
      }

      const logicKey = label.nextElementSibling.querySelector('.el-input__inner').title;
      const newUrl = getUrlFromLogs(logicKey, true);
      if (!newUrl) {
        _removeLookLogicButton(label);
        _createCopyNameButton(label, null);
        continue;
      }

      // 按日志反查逻辑名(展示与复制名用),键用逻辑 id
      const logicName = findDesignNameById(logicKey) ?? '';

      _createOpenLogicButton(label, logicKey, logicName, () => window.open(newUrl, '_blank'));
    }
  }

  // 获取选中的逻辑名称
  function _getSelectedLogicName() {
    const logicNameElements = document.querySelectorAll(
      '.el-scrollbar__view.el-select-dropdown__list > .el-select-dropdown__item.selected > span',
    );
    let logicName = null;
    for (const nameElement of logicNameElements) {
      const text = nameElement.innerText.trim();
      if (['string', '公式'].includes(text)) {
        continue;
      }
      if (!logicName || text.length > logicName.length) {
        logicName = text;
      }
    }
    return logicName;
  }

  /**
   * 创建查看按钮(工厂函数)
   * @param {Element} target - 宿主 label
   * @param {string} key - 按钮身份键(target-key,可为逻辑 id 或逻辑名)
   * @param {string} name - 展示与复制名跟随的逻辑名
   * @param {Function} onClick
   */
  function _createOpenLogicButton(target, key, name, onClick) {
    _createLogicNameDisplay(target, name);

    const button = ensureInjected({
      host: target,
      find: '.ze-look-logic-button',
      keyAttr: 'target-key',
      key,
      mount() {
        const newButton = createButton({
          text: '查看',
          className: 'ze-look-logic-button',
          dataset: { 'target-key': key },
          onClick,
        });
        newButton.style.marginLeft = '10px';
        target.appendChild(newButton);
        return newButton;
      },
    });

    _createCopyNameButton(target, name);
    return button;
  }

  // 移除查看逻辑按钮
  function _removeLookLogicButton(target) {
    const buttonClass = 'ze-look-logic-button';
    const existedButton = target.querySelector('.' + buttonClass);
    if (existedButton) {
      existedButton.remove();
    }
    _createLogicNameDisplay(target, null);
  }

  // 检查 logicName 是否变化,如变化则移除旧按钮并返回是否需要继续处理
  function _updateButtonsIfNeeded(label) {
    const existedButton = label.querySelector('.ze-look-logic-button');
    const existedLogicName = existedButton?.getAttribute('target-key') || null;
    const logicName = _getSelectedLogicName();

    // 如果 logicName 没变化,跳过处理
    if (logicName === existedLogicName) {
      return null;
    }

    // 没有逻辑名称或发生变化时,移除旧按钮
    _removeLookLogicButton(label);
    _createCopyNameButton(label, null);
    return logicName;
  }

  // 创建逻辑名称展示(只读,工厂函数)
  function _createLogicNameDisplay(target, logicName) {
    const displayClass = 'ze-logic-name-display';
    return ensureInjected({
      host: target,
      find: '.' + displayClass,
      keyAttr: 'logic-name-11ze',
      key: logicName,
      mount() {
        if (!logicName) return null;
        const span = document.createElement('span');
        span.className = displayClass;
        span.textContent = logicName;
        span.setAttribute('logic-name-11ze', logicName);
        target.appendChild(span);
        return span;
      },
    });
  }

  // 创建复制按钮(工厂函数)
  function _createCopyNameButton(target, logicName) {
    const buttonClass = 'ze-copy-logic-name-button';
    return ensureInjected({
      host: target,
      find: '.' + buttonClass,
      keyAttr: 'target-logic-name',
      key: logicName,
      mount() {
        if (!logicName) return null;
        const copyButton = createButton({
          text: '复制',
          className: buttonClass,
          dataset: { 'target-logic-name': logicName },
          type: 'default',
          onClick: (e) => {
            e.preventDefault();
            e.stopPropagation();
            Utils.copyToClipboard(logicName, copyButton, '已复制');
          },
        });
        copyButton.style.marginLeft = '10px';
        target.appendChild(copyButton);
        return copyButton;
      },
    });
  }

  /**
   * 新版逻辑嵌套组件,检查到【逻辑嵌套】组件时,自动添加一个按钮用于查看对应的逻辑设计
   * 从已打开过的逻辑设计中获取跳转链接
   */
  function firstAddButtonToOpenNewLogicDesignForNestedLogic() {
    const labels = document.querySelectorAll('.el-form-item__label');
    for (const label of labels) {
      if (!label.innerText.includes('选择逻辑引擎')) {
        continue;
      }

      const logicName = _updateButtonsIfNeeded(label);
      if (!logicName) {
        continue;
      }

      const jvsAppId = getJvsAppId();
      const newUrl = getUrlFromLogsAndUrl(logicName, jvsAppId);
      if (!newUrl) {
        secondAddButtonToOpenNewLogicDesignForNestedLogic();
        return;
      }

      _createOpenLogicButton(label, logicName, logicName, () => window.open(newUrl, '_blank'));
      return;
    }
  }

  /**
   * 新版逻辑嵌套组件,检查到【逻辑嵌套】组件时,自动添加一个按钮用于查看对应的逻辑设计
   * 从左上角的 icon 中获取跳转页面
   * 谨慎使用,点到引用按钮会覆盖当前逻辑设计,自动保存,不可逆
   */
  function secondAddButtonToOpenNewLogicDesignForNestedLogic() {
    const labels = document.querySelectorAll('.el-form-item__label');

    const otherRuleListIcon = document.querySelector('.rule-list-icon');
    if (!otherRuleListIcon) {
      return;
    }
    if (!otherRuleListIcon.getAttribute('check-logic-design-rule-list-icon-11ze')) {
      // 点击后才有逻辑列表
      otherRuleListIcon.click();
      otherRuleListIcon.click();
    }
    otherRuleListIcon.setAttribute('check-logic-design-rule-list-icon-11ze', 'true');

    const otherRuleList = document.querySelectorAll('.other-rule-list > .list-box > .list-item');

    for (const label of labels) {
      if (!label.innerText.includes('选择逻辑引擎')) {
        continue;
      }

      const logicName = _updateButtonsIfNeeded(label);
      if (!logicName) {
        continue;
      }

      for (const otherRule of otherRuleList) {
        const otherRuleText = otherRule?.textContent?.trim();
        if (!otherRuleText) {
          continue;
        }

        if (otherRuleText === logicName) {
          _createOpenLogicButton(label, logicName, logicName, () => {
            // 第一个按钮是「设计」,第二个「引用」
            // 点击引用会引用设计覆盖当前逻辑设计,自动保存,不可逆
            const desginButtons = otherRule
              .querySelector('.list-item-tool')
              .querySelectorAll('div.el-tooltip');
            if (desginButtons.length === 2) {
              desginButtons[0].click();
            }
          });
          return;
        }
      }
    }
  }

  // 新版 JVS,在逻辑设计名称旁边添加复制按钮
  function addButtonToCopyDesignName() {
    // 逻辑设计
    let designName = document.querySelector(
      '#app > div > div > div.design-header-box > div.header-left > span',
    );
    if (!designName) {
      // 表单设计
      designName = document.querySelector(
        '#app > div > div > div:nth-child(1) > div.design-header-box > div.header-left > span',
      );
    }

    if (designName) {
      const designNameText = designName.innerText.trim();

      ensureInjected({
        host: document,
        find: '#copy-design-name-button-11ze',
        keyAttr: 'design-name-11ze',
        key: designNameText,
        mount() {
          // 名称旁无 use 图标(编辑态图标)时不注入;键异时旧按钮已在上文移除
          if (!designName.querySelector('use')) {
            return null;
          }

          const copyButton = createButton({
            text: '复制',
            id: 'copy-design-name-button-11ze',
            dataset: { 'design-name-11ze': designNameText },
            onClick: () => Utils.copyToClipboard(designNameText, copyButton, '已复制'),
          });
          copyButton.style.marginLeft = '10px';
          designName.parentNode.insertBefore(copyButton, designName.nextSibling);
          return copyButton;
        },
      });
    }
  }

  /**
   * 新版 JVS,添加按钮复制组件名称
   */
  function addButtonToCopyComponentName() {
    const buttonClass = 'ze-copy-component-name-button';

    const componentName = document.querySelector('#node_detailpannel > h4 > div > span');
    if (!componentName) {
      return;
    }

    // 不在旧版加按钮,如果父级有子元素 el-icon-document-copy,则 return
    if (componentName.parentNode.querySelector('.el-icon-document-copy')) {
      STATE.skipCopyComponentButton = true;
      return;
    }

    // 单向闩锁有意不复位:旧版站点图标消失后也不再注入(会话内判定一次即定)
    if (STATE.skipCopyComponentButton) {
      return;
    }

    const componentNameText = componentName.innerText.trim();

    ensureInjected({
      host: document,
      find: '#copy-component-name-button-11ze',
      keyAttr: 'component-name-11ze',
      key: componentNameText,
      mount() {
        const copyButton = createButton({
          text: '复制',
          id: 'copy-component-name-button-11ze',
          className: buttonClass,
          dataset: { 'component-name-11ze': componentNameText },
          onClick: () => Utils.copyToClipboard(componentNameText, copyButton, '已复制'),
        });
        copyButton.style.marginLeft = '10px';
        componentName.parentNode.insertBefore(copyButton, componentName.nextSibling);
        return copyButton;
      },
    });
  }

  /**
   * 逻辑设计,添加按钮一键清空所有字段
   */
  function addButtonToClearAllFields() {
    const boxes = document.querySelectorAll('.data-model-box');

    for (let i = 0; i < boxes.length; i++) {
      const box = boxes[i];
      ensureInjected({
        host: box,
        find: '#clear-all-fields-button-11ze' + i,
        mount() {
          const button = createButton({
            text: '清空',
            id: 'clear-all-fields-button-11ze' + i,
            onClick: () => {
              const ps = box.querySelectorAll('p');
              for (let i = ps.length - 1; i >= 0; i--) {
                const el = ps[i];
                if (el.querySelector('.delete-icon-button')) {
                  el.querySelector('.delete-icon-button > span').click();
                }

                if (el.querySelector('.el-icon-delete')) {
                  el.querySelector('.el-icon-delete').click();
                }
              }
            },
          });
          box.insertBefore(button, box.firstChild);
          return button;
        },
      });
    }
  }

  /**
   * 在列表表单列表的 id 旁边添加查看按钮
   */
  function addButtonToOpenNewFormOrListDesign() {
    // 列表设计页面也有元素,得排除
    const tabType = getTabType();
    if (tabType === '列表设计') {
      return;
    }

    const selector =
      'div.table-body-box > div > div.el-table__body-wrapper.is-scrolling-none > table > tbody > tr > td:nth-child(2) > div > span > span > div';
    const elements = document.querySelectorAll(selector);

    for (const element of elements) {
      const designId = element.innerText.trim();
      if (!designId) {
        continue;
      }

      element.style.whiteSpace = 'normal';

      // 行级标记是性能闩锁:未标记的行才扫日志,避免每 tick 全表重查
      if (Utils.isMarked(element, 'form-added-button-11ze')) {
        continue;
      }

      const targetUrl = getUrlFromLogs(designId, false);
      if (!targetUrl) {
        continue;
      }

      const targetElement =
        element.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector(
          'td:nth-child(5) > div > div',
        );
      const mounted = ensureInjected({
        host: targetElement,
        find: '#open-new-form-or-list-design-button-11ze',
        mount() {
          const copyButton = createButton({
            text: '查看',
            id: 'open-new-form-or-list-design-button-11ze',
            onClick: () => window.open(targetUrl, '_blank'),
          });
          targetElement.appendChild(copyButton);
          return copyButton;
        },
      });
      if (mounted) {
        Utils.mark(element, 'form-added-button-11ze');
      }
    }
  }

  /**
   * 自动展开表单设计所有组件设置
   */
  function expandFormDesignAllComponentSettings() {
    // 表单设计
    const tabType = getTabType();
    if (tabType !== '表单设计') {
      return;
    }

    // 文字标题元素跟展开内容元素同级
    const buttons = document.querySelectorAll('.el-collapse-item > .el-collapse-item__wrap');
    for (const button of buttons) {
      if (Utils.isMarked(button, 'bottom-body-checked-11ze')) {
        continue;
      }

      const text = button.parentElement.innerText.trim();

      const targetNames = ['设置', '扩展', '功能', '校验'];

      for (const targetName of targetNames) {
        if (text.includes(targetName)) {
          button.style.display = 'block';
          break;
        }
      }

      Utils.mark(button, 'bottom-body-checked-11ze');
    }
  }

  /**
   * 高亮应用中心的应用
   */
  function highlightApps() {
    const starClass = 'ze-star-btn';
    const markedClass = 'ze-marked';

    const STAR_SVG =
      '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';

    function getContentSelector() {
      return 'div > div > div > p';
    }

    // 每次现读存储:点击闭包不持有列表快照,多标签页同开不会互相覆盖
    function getMarkedApps() {
      return jvsStorage.get(STORAGE_KEYS.HIGHLIGHT_APPS, []);
    }

    function handle(nodes) {
      const markedApps = getMarkedApps();
      // 如果在标记列表中,就点亮星标
      nodes.forEach((n) => {
        const star = n.querySelector(`.${starClass}`);
        if (!star) {
          return;
        }

        star.classList.toggle(markedClass, markedApps.includes(getNodeText(n)));
      });
    }

    function getNodeText(node) {
      return node.querySelector(getContentSelector()).innerText.trim();
    }

    function handleClickNode(node) {
      const text = getNodeText(node);
      const markedApps = getMarkedApps();

      if (markedApps.includes(text)) {
        markedApps.splice(markedApps.indexOf(text), 1);
      } else {
        markedApps.push(text);
      }
      jvsStorage.set(STORAGE_KEYS.HIGHLIGHT_APPS, markedApps);
    }

    function main() {
      const containerSelector = '.application';

      // 找到相关数据项,不存在就结束
      const nodes = [...document.querySelectorAll(containerSelector)];
      if (!nodes.length) return;

      // 注入星标按钮:平时隐藏,悬停卡片浮现,点击切换标记
      nodes.forEach((n) => {
        ensureInjected({
          host: n,
          find: '.' + starClass,
          mount() {
            const star = document.createElement('button');
            star.className = starClass;
            star.title = '星标应用';
            star.innerHTML = STAR_SVG;
            star.addEventListener('click', (event) => {
              event.stopPropagation();
              handleClickNode(n);
              // 立即刷新当前卡片,不等自动轮询
              handle([n]);
            });
            n.appendChild(star);
            return star;
          },
        });
      });

      // 渲染
      handle(nodes);
    }

    main();
  }

  /**
   * 应用中心「只看星标」过滤开关
   * JS 只产出 body class 这一个事实,卡片/空组隐藏与空态提示全由 CSS :has() 驱动;
   * 状态现读存储(多标签页同开自动一致),离开应用中心时清理 body class。
   * 新版应用中心容器是 .app-page,旧版是 .jvs-layout-tempOpen 下的 .template-content-box,
   * 按钮优先注入旧版筛选行(.filter-bar)搜索框右侧,无筛选行才钉容器顶部
   */
  function filterStarredApps() {
    const btnClass = 'ze-star-filter-btn';
    const bodyClass = 'ze-star-filter-on';

    function isFilterOn() {
      return jvsStorage.get(STORAGE_KEYS.STARRED_FILTER, false);
    }

    function syncFilterState(btn = document.querySelector('.' + btnClass)) {
      const on = isFilterOn();
      document.body.classList.toggle(bodyClass, on);
      if (btn) {
        btn.setAttribute('aria-pressed', on);
      }
    }

    const appPage =
      document.querySelector('.app-page') ??
      document.querySelector('.jvs-layout-tempOpen > .template-content-box');
    if (!appPage) {
      // 开关按钮是页面容器子节点,随 SPA 路由切换销毁;body class 要手动清
      document.body.classList.remove(bodyClass);
      return;
    }

    // 旧版筛选行(全部分类 + 搜索框)是静态标记、挂载即渲染;卡片列表由异步数据 v-for、
    // 晚于筛选行出现。按钮与提示分别判重:提示只在第一张卡出现后插进其父级(卡片流开头),
    // 卡片没出来就等下一个 tick(host 缺失守卫)——往容器 prepend 会把提示顶到筛选行上方
    const filterBar = appPage.querySelector('.filter-bar');
    const firstCard = appPage.querySelector('.application');
    const tipHost = filterBar ? firstCard?.parentElement : appPage;
    ensureInjected({
      host: tipHost,
      find: '.ze-star-empty-tip',
      mount() {
        const tip = document.createElement('div');
        tip.className = 'ze-star-empty-tip';
        tip.innerText = '暂无星标应用(悬停应用卡片点亮 ★)';
        tipHost.prepend(tip);
        return tip;
      },
    });
    const btn = ensureInjected({
      host: filterBar ?? appPage,
      find: '.' + btnClass,
      mount() {
        const created = document.createElement('button');
        created.className = btnClass;
        created.type = 'button';
        created.innerText = '★ 只看星标';
        created.addEventListener('click', () => {
          jvsStorage.set(STORAGE_KEYS.STARRED_FILTER, !isFilterOn());
          syncFilterState();
        });
        filterBar ? filterBar.append(created) : appPage.prepend(created);
        return created;
      },
    });

    syncFilterState(btn);
  }

  /**
   * 应用中心侧边栏展开/收起开关
   * 与 filterStarredApps 同一范式:JS 只产出 body class,侧边栏隐藏与卡片列
   * 拉满全由 CSS 驱动;状态现读存储(刷新保持、多标签页同开自动一致)。
   * 按钮挂在 .sidebar-col 父级容器内,SPA 路由切换随容器销毁;
   * 侧边栏收起后 rect 归零不可读,按钮 left 直接按状态取值
   */
  function toggleAppCenterSidebar() {
    const btnClass = 'ze-side-toggle-btn';
    const bodyClass = 'ze-appcenter-side-collapsed';

    function isCollapsed() {
      return jvsStorage.get(STORAGE_KEYS.APPCENTER_SIDEBAR, false);
    }

    function sync() {
      const collapsed = isCollapsed();
      document.body.classList.toggle(bodyClass, collapsed);
      btn.innerText = collapsed ? '»' : '«';
      btn.setAttribute('aria-pressed', collapsed);
      btn.style.left = (collapsed ? 6 : sidebar.getBoundingClientRect().right - 16) + 'px';
    }

    const sidebar = document.querySelector('.sidebar-col');
    if (!sidebar) {
      // 开关按钮随宿主容器销毁;body class 要手动清
      document.body.classList.remove(bodyClass);
      return;
    }

    const btn = ensureInjected({
      host: sidebar.parentElement,
      find: '.' + btnClass,
      mount() {
        const created = document.createElement('button');
        created.className = btnClass;
        created.type = 'button';
        created.title = '展开/收起侧边栏';
        created.addEventListener('click', () => {
          jvsStorage.set(STORAGE_KEYS.APPCENTER_SIDEBAR, !isCollapsed());
          sync();
        });
        sidebar.parentElement.appendChild(created);
        return created;
      },
    });

    sync();
  }

  /**
   * 逻辑设计,自动展开组件库里指定的分类
   */
  function autoExpandComponentLibraryCategory() {
    if (STATE.componentLibraryExpanded) {
      return;
    }

    const ruleCategories = document.querySelectorAll('.left-tool-list-box > .rule-assembly-list');

    if (ruleCategories.length === 0) {
      return;
    }

    for (const ruleCategory of ruleCategories) {
      if (ruleCategory.classList.contains('open')) {
        continue;
      }

      if (Utils.isMarked(ruleCategory, 'auto-expand-component-library-category-11ze')) {
        continue;
      }

      const textDom = ruleCategory.querySelector('div > div > .label');
      if (!textDom) {
        continue;
      }

      const text = textDom.innerText.trim();
      if (['模型插件', '服务插件', '自定义代码插件'].includes(text)) {
        const left = ruleCategory.querySelector('.t-left');
        if (left) {
          left.dispatchEvent(new MouseEvent('click', { bubbles: true }));
        }
      }

      Utils.mark(ruleCategory, 'auto-expand-component-library-category-11ze');
    }

    function simulateMouseClick(element) {
      const mouseClickEvents = ['mousedown', 'click', 'mouseup'];

      mouseClickEvents.forEach((mouseEventType) => {
        const mouseEvent = new MouseEvent(mouseEventType, {
          bubbles: true, // 允许事件冒泡
          cancelable: true, // 允许事件被取消
          clientX: element.clientWidth, // 设置点击位置
          clientY: element.clientHeight, // 设置点击位置
        });
        element.dispatchEvent(mouseEvent);
      });
    }

    // 鼠标左键点击一次,收起组件库
    const container = document.querySelector('.butterfly-wrapper');
    if (container) {
      simulateMouseClick(container);
    }

    STATE.componentLibraryExpanded = true;
  }

  function getAppNameMap() {
    return jvsStorage.get(STORAGE_KEYS.APP_NAME_MAP, {});
  }

  function getAppIdName(jvsAppId) {
    const appNameMap = getAppNameMap();
    return appNameMap[jvsAppId] ?? '';
  }

  function saveAppIdName(jvsAppId, appName) {
    if (appName === '复制') {
      return;
    }

    if (!jvsAppId || !appName) {
      return;
    }

    const appNameMap = getAppNameMap();

    if (appNameMap[jvsAppId]) {
      return;
    }

    appNameMap[jvsAppId] = appName;
    appNameMap[appName] = jvsAppId;
    jvsStorage.set(STORAGE_KEYS.APP_NAME_MAP, appNameMap);
  }

  function applicationSetClick() {
    const applicationElements = document.querySelectorAll('div.application');

    if (applicationElements.length === 0) {
      return;
    }

    // 使用命名函数以便后续可以移除事件监听器
    function handleApplicationClick(event) {
      const clickedElement = event.currentTarget;

      const idElement = clickedElement.querySelector('label.el-checkbox span.el-checkbox__label');
      const nameElement = clickedElement.querySelector('p');

      let applicationId = null;
      let applicationName = null;

      if (idElement) {
        applicationId = idElement.textContent.trim();
      } else {
        console.warn('11ze 未找到 ID 元素:', clickedElement);
      }

      if (nameElement) {
        applicationName = nameElement.textContent.trim();
      } else {
        console.warn('11ze 未找到名称元素:', clickedElement);
      }

      if (applicationId !== null && applicationName !== null) {
        saveAppIdName(applicationId, applicationName);
      } else {
        console.warn('11ze 应用中心点击应用未能完整获取点击的应用信息:', clickedElement);
      }
    }

    applicationElements.forEach(function (appElement) {
      if (appElement.classList.contains('set-click-11ze')) {
        return;
      }

      appElement.addEventListener('click', handleApplicationClick);
      appElement.classList.add('set-click-11ze');
    });
  }

  /**
   * 逻辑设计,在组件右边显示执行时间
   */
  function showNodeExecTime() {
    const nodes = document.querySelectorAll(
      '.el-icon-circle-check.el-node-state-success.el-popover__reference',
    );

    // 在 aria-describedby 属性有 id
    for (const node of nodes) {
      const id = node.getAttribute('aria-describedby');
      if (!id) {
        continue;
      }

      const timeDomId = id + '-time-11ze';

      const resultPopover = document.getElementById(id);
      if (!resultPopover) {
        document.getElementById(timeDomId)?.remove();
        continue;
      }

      const resultText = resultPopover.querySelector('div > h4 > span > span');
      if (!resultText) {
        document.getElementById(timeDomId)?.remove();
        continue;
      }

      const resultTextContent = resultText.textContent.trim();
      if (!resultTextContent) {
        document.getElementById(timeDomId)?.remove();
        continue;
      }

      // 键=结果文本:耗时变化时移除旧标注重建
      ensureInjected({
        host: node.parentNode,
        find: '#' + timeDomId,
        keyAttr: 'exec-time-11ze',
        key: resultTextContent,
        mount() {
          const span = document.createElement('span');
          span.id = timeDomId;
          span.setAttribute('exec-time-11ze', resultTextContent);
          span.textContent = resultTextContent;
          span.style.position = 'absolute';
          span.style.right = '-40px';
          span.style.color = 'red';
          node.parentNode.appendChild(span);
          return span;
        },
      });
    }
  }

  /** 画布滚轮平移的探测状态:上次见到的画布实例 / 画布身份 / 销毁前最后已知平移(连线等操作会让应用整体重建画布,平移归零)/ 重建代数 */
  const canvasScrollState = {
    seen: null,
    identity: null,
    offset: null,
    rebuildCount: 0,
  };

  /** 读当前画布身份:主/循环画布切换器的高亮项(主画布/循环容器),取不到给空串 */
  function getCanvasIdentity() {
    return document.querySelector('.canvas-tool-item.active')?.textContent?.trim() || '';
  }

  /** 取画布(container 就绪且 canvas 带 move/getOffset 能力),否则 null */
  function getButterflyCanvas() {
    const container = document.querySelector('.butterfly-vue-container');
    const canvas = container?.parentElement?.__vue__?.canvas;
    if (!canvas || typeof canvas.move !== 'function' || typeof canvas.getOffset !== 'function') {
      return null;
    }
    return { container, canvas };
  }

  /** 实例已更换且记录了非零平移时返回待恢复的平移,否则 null */
  function canvasScrollRestoreOf(canvas) {
    if (canvas === canvasScrollState.seen) {
      return null;
    }
    const [offsetX, offsetY] = canvasScrollState.offset || [0, 0];
    return offsetX === 0 && offsetY === 0 ? null : canvasScrollState.offset;
  }

  /**
   * 给逻辑设计的画布挂滚轮平移(wheel → Butterfly canvas.move)
   * 全走画布 API:坐标系服务同步(不与左键拖拽冲突),小地图视口框由站点原生联动;
   * 主/循环画布切换时容器整体重建,闩锁随 DOM 销毁,由轮询调度自动重挂;
   * 同一画布内重建(连线等)把销毁前的平移带回来,切换画布时平移记录直接作废
   */
  function setCanvasScroll() {
    const { container, canvas } = getButterflyCanvas();
    const restore = canvasScrollRestoreOf(canvas);
    if (restore) {
      canvas.move(restore);
    }
    canvasScrollState.seen = canvas;
    if (container.getAttribute('data-11ze-canvas-scroll')) {
      return;
    }
    container.addEventListener(
      'wheel',
      (e) => {
        e.preventDefault();
        const offset = canvas.getOffset();
        const deltaX = e.deltaX || (e.shiftKey ? e.deltaY : 0);
        const deltaY = deltaX ? 0 : e.deltaY;
        canvas.move([offset[0] - deltaX, offset[1] - deltaY]);
      },
      { passive: false }
    );
    container.setAttribute('data-11ze-canvas-scroll', 'true');
  }

/**
 * favicon 图标映射(Base64 编码的 PNG 图片)
 * 键为设计类型简称:列、表、逻、流
 */
const ICONS = {
  列表设计:
    'data:image/png;base64,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',
  表单设计:
    'data:image/png;base64,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',
  逻辑设计:
    'data:image/png;base64,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',
  流程设计:
    'data:image/png;base64,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',
};

/**
 * CSS 样式字符串
 * 包含所有需要注入页面的样式规则
 */
const JVS_STYLES = `
  /* 修改模型信息的弹窗宽度 */
  body > div > div.el-dialog[aria-label="修改模型"],
  body > div > div.el-dialog[aria-label="数据集详情"],
  body > div > div.el-dialog[aria-label="数据模型配置"] {
    width: 75% !important;
  }

  /* 列表设计的按钮设置弹窗宽度 */
  body > div > div.el-dialog[aria-label="导出"],
  body > div > div.el-dialog[aria-label="下载模板"] {
    width: 75% !important;
  }

  /* 逻辑设计,调整画布侧边栏宽度 */
  .canvas-tool {
    width: 260px !important;
  }

  /* 逻辑设计,画布列表的图标 */
  .canvas-tool-item > svg {
    display: none;
  }

  /* 调整画布右边的按钮位置 */
  .itempannel-box {
    left: 280px !important;
  }

  /* 新版 JVS,逻辑设计,把浮动操作栏的宽度减小到 45% */
  .tool-bar {
    width: 45% !important;
  }

  /* 新版 JVS,逻辑设计,组件详情底部的按钮,设置高度,原本 32px,改成 72px */
  #node_detailpannel > div.block-container > div > form > div.el-row > div.form-item-btn.el-col.el-col-24 > div > div {
    .el-button,
    span {
      height: 72px !important;
      width: 90px !important;
    }
  }

  /* 逻辑设计,展开组件库的组件名称(换行) */
  .left-tool-list {
    li.getItem > span {
      white-space: normal !important;
    }
  }

  /* 表单设计,展开按钮设置 */
  .item-body {
    display: block !important;
  }

  /* 表单设计,悬停时组件名不消失:站点 hover 规则 .formitem:not(.clicked):hover
     会把 .type-name 藏掉(只留复制/删除图标),常显功能须盖回 */
  .active-formitem2 .handle-btn .type-name {
    display: block !important;
  }

  /* 逻辑设计,展开变量组件设置里的按钮 */
  .bottom-body {
    display: block !important;
  }

  /* 展开 JVS 应用名称和侧边栏功能名称 */
  /* 首页 */
  #app > div > div > div.jvs-layout > div.jvs-main > div.el-scrollbar > div.el-scrollbar__wrap > div > div > div.top-outer-container > div.el-row-bottom-container.el-row > div > div > div > div > p,
  /* 旧应用中心 */
  #template > div.template-manage-content > div.template-manage-box > div.my-template-list > div > div > div.content > div.content-header > h5,
  /* 新首页 > 常用应用 */
  #app > div > div > div.jvs-layout > div.jvs-main > div.el-scrollbar > div.el-scrollbar__wrap > div > div > div.top-outer-container > div > div:nth-child(2) > div > div:nth-child(4) > div > div.card-body.el-col.el-col-24 > div > div > p,
  /* 新首页 > 应用中心 */
  #app > div > div > div.jvs-layout.jvs-layout-tempOpen > div.template-content-box > div.container.el-row > div:nth-child(2) > div > div > div > div:nth-child(1) > p,
  /* 应用左上角 */
  #app > div > div > div.jvs-layout > div.jvs-left > div > div.el-menu-scrollbar.el-scrollbar > div.el-scrollbar__wrap > div > div.app-item-info.app-item-info-hide > div > span,
  #app > div > div > div.jvs-layout > div.jvs-left > div > div.el-menu-scrollbar.el-scrollbar > div.el-scrollbar__wrap > div > div.app-item-info > div > span,
  /* 逻辑设计、列表设计、表单设计 */
  #app > div > div > div.design-header-box > div.header-left {
    white-space: normal !important;
  }

  /* 放大公式 icon,原本 16px */
  .add-formula-svg {
    width: 22px !important;
    height: 22px !important;
  }

  /* 移除新版逻辑设计右下角遮挡按钮的透明条 */
  .cont-box-right > .tool-bar {
    display: none !important;
  }

  /* 逻辑设计,展开组件名称(.ef-node-text 折行后的善后规则见下方旧版节点块) */
  .ef-node-text,
  .canvas-tool-item {
    white-space: normal !important;
    width: 100% !important;
    color: #363b4c !important;
  }

  /* 旧版 JVS,逻辑设计,展开组件名称折行后在原文字框内显示两行(行高 20px、框内居中),节点框与连线锚点不动 */
  .jvs-rule-node.ef-node-container.jtk-droppable {
    .ef-node-text {
      line-height: 20px !important;
      min-height: 48px !important;
      display: flex !important;
      flex-direction: column !important;
      justify-content: center !important;
    }
  }

  /* 新版 JVS,逻辑设计,框与 .top 维持原生固定高(连线坐标走原生逻辑),只放开文本层溢出——
     折行名称完整画到框外,不占布局、不推锚点;:not 隔离旧版节点。
     节点本体严禁加定位:原生 static,inline 里的 top/left 是应用写的画布坐标冗余,一激活就飞 */
  .jvs-rule-node.ef-node-container:not(.jtk-droppable) .ef-node-text {
    overflow: visible !important;
  }

  /* 新版 JVS,逻辑设计,隐藏节点左侧图标(类名带 flow-node-drag 但实测非拖拽把手,
     拖拽走整个节点),文字左移占满 */
  .jvs-rule-node.ef-node-container:not(.jtk-droppable) .ef-node-left-ico {
    display: none !important;
  }

  /* 新版 JVS,逻辑设计,右侧执行结果图标槽空置时收起(有结果时应用给槽内 reference
     图标挂 el-node-state-success/el-node-state-error 字体图标类,空置时没有——
     图标是字体不是 svg,:empty 也不可用);文字层 width:100% 自动占满,
     有执行结果图标时恢复原生占位不压字 */
  .jvs-rule-node.ef-node-container:not(.jtk-droppable) .ef-node-right-ico:not(:has([class*="el-node-state"])) {
    display: none !important;
  }

  /* 新版 JVS,逻辑设计,所有节点统一精修:可见的中性灰实线描边(原生是 2px 白色占位边框)、
     8px 圆角、轻投影;常驻实线顺带压掉应用原生 hover 的虚线边框 */
  .jvs-rule-node.ef-node-container:not(.jtk-droppable) {
    border: 1px solid #c0c4cc !important;
    border-radius: 8px !important;
    box-shadow: 0 1px 3px rgba(31, 45, 61, 0.12) !important;
  }

  /* 新版 JVS,逻辑设计,命中类型的节点(paint 端加 ze-typed 类与颜色变量)换同色深一阶描边 */
  .jvs-rule-node.ef-node-container.ze-typed:not(.jtk-droppable) {
    border-color: color-mix(in srgb, var(--ze-type-color), #303133 30%) !important;
  }

  /* 新版 JVS,逻辑设计,只把静止灰线的虚线改实线(粗细、颜色都保持原生)、端点显形为
     白底圆点(原生透明不可见);:not 排除的是应用状态类词表快照(success 绿虚线是日志
     回放动画,error/async/abnormal/active 同为状态线)——应用加新状态类须回来补,
     否则新状态线会被压平成实线;不写 stroke/stroke-width——线色线宽都由应用控制;
     Butterfly 特有类名,旧版 jsPlumber 天然不冲突 */
  .butterfly-svg path.butterflies-link:not(.active, .success, .error, .abnormal, .async) {
    stroke-dasharray: none !important;
  }

  .butterfly-wrapper .butterflie-circle-endpoint {
    border-radius: 50% !important;
    background: #fff !important;
    border: 1px solid #909399 !important;
    box-sizing: border-box !important;
  }

  /* 新版 JVS,逻辑设计,入边箭头加大加深后微缩(原生 8宽×8高浅灰实心小三角
     不起眼);left 补偿值 = 左右 border 的一半,保持水平居中;尖角钉在节点
     顶边(尖角 = 盒顶 + 三角高,top 取负的三角高;原生扎进节点 4px 的位置
     被用户否决);:not(.jtk-droppable) 隔离旧版(旧版节点同带这些类,见上方节点规则) */
  .jvs-rule-node.ef-node-container:not(.jtk-droppable) .top-endpoint {
    top: -10px !important;
    left: calc(50% - 6px) !important;
    border-width: 10px 6px 4px !important;
    border-top-color: #909399 !important;
  }

  /* 逻辑设计,调整页面设置和已使用逻辑的宽度 */
  .content-box:has(.page-setting),
  .content-box:has(.used-logic),
  .content-box:has(.permission-page) {
    width: 80% !important;
    margin-left: 10% !important;
  }

  .content-box > .page-setting,
  .content-box > .used-logic,
  .content-box > .permission-box,
  .content-box > .permission-page {
    width: 99% !important;
  }

  /* 逻辑设计,设计名称输入框 */
  #app > div > div > div.design-header-box > div.header-left > div.el-input.el-input--mini > input.el-input__inner {
    width: 300px !important;
  }

  /* 逻辑设计,设计名称编辑 icon */
  #app > div > div > div.design-header-box > div.header-left > span > svg {
    width: 22px !important;
    height: 22px !important;
  }

  /* 逻辑设计,设置栏「结构定义」按钮与「测试」拉开间距(应用 inline 写死 10px,挨得太近,
     须 !important 才能盖过 inline);「测试」按钮外包两层 span 且在自己盒内 margin-left 10px、
     溢出 span 盒子右侧,会吃掉本规则的间距——实测需 22px 才比左缝(10px)宽 */
  .el-form-item.form-btn-bar .el-form-item__content > button.el-button:last-of-type {
    margin-left: 22px !important;
  }

  /* 自己加的组件 */

  /* 日志弹窗 */
  .popup-11ze {
    border: 1px solid rgba(0, 0, 0, 0.1) !important;
    border-radius: 10px !important;
    box-shadow:
      0 4px 12px rgba(0, 0, 0, 0.1),
      0 8px 24px rgba(0, 0, 0, 0.15) !important;
    background-color: #fff !important;
    position: fixed;
    top: 50px;
    right: 310px; /* 默认值,由 JS 动态覆盖 */
    z-index: 9999;
    padding: 0 10px 10px 10px;
    max-height: 800px;
    overflow-y: auto;
  }

  .table-11ze {
    font-size: 14px !important;
    min-width: 600px !important;
    border-bottom: 1px solid #dddddd !important;
    text-align: left !important;
  }

  /* 日志弹窗的表格 */
  .log-11ze-table-tr > td,
  .log-11ze-table-tr > th {
    padding: 4px 4px !important;
    border-bottom: 1px solid #e5e7eb !important;
    border-right: 1px solid #e5e7eb !important;
  }

  /* 表头样式 */
  .log-11ze-table-tr > th {
    background-color: #f8fafc !important;
    border-bottom: 2px solid #e2e8f0 !important;
    position: sticky !important;
    z-index: 1 !important;
  }

  /* 最后一行不显示分割线 */
  .log-11ze-table-tr:last-child > td {
    border-bottom: none !important;
  }

  /* 最后一列不显示分割线 */
  .log-11ze-table-tr > td:last-child,
  .log-11ze-table-tr > th:last-child {
    border-right: none !important;
  }

  .log-11ze-table-tr > td:hover {
    background-color: #E8F4FF; /* 悬停时的背景颜色 */
  }

  /* 名称列鼠标变为小手 */
  .log-design-name-11ze {
    cursor: pointer;
  }

  .log-11ze-select {
    display: inline-block;
    background-color: white !important;
    border-color: #D6E4FF !important;
    color: black !important;
    border: 1px solid #D6E4FF !important;
    font-size: 14px !important;
    border-radius: 6px !important;
  }

  .log-11ze-select:hover {
    background-color: #E8F4FF !important;
    cursor: pointer;
  }

  /* 日志栏共享样式:仿应用内 Element plain 蓝 */
  .button-11ze,
  .drag-handle-11ze {
    background-color: white !important;
    color: #409EFF !important;
    border: 1px solid #B3D8FF !important;
    border-radius: 8px !important;
    padding: 6px 10px !important;
    margin: 2px 4px !important;
    font-size: 13px !important;
    transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
      border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
  }

  .button-11ze:hover,
  .drag-handle-11ze:hover {
    background-color: #ECF5FF !important;
    border-color: #409EFF !important;
  }

  /* 日志按钮整钮随模式换色:规则由 buildModeColorCss 从 MODE_COLORS 生成(文件末尾拼接注入) */

  .button-11ze:hover {
    cursor: pointer;
  }

  /* 日志按钮紧贴拖拽把手,去除中间间距 */
  #ze-jvs-log-button {
    margin-right: 0 !important;
  }

  .drag-handle-11ze {
    margin: 2px 0 !important;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: move;
    user-select: none;
  }

  .ze-logic-name-display {
    display: block;
    width: 100%;
    margin: 4px 0 2px 0;
    padding: 2px 0;
    font-size: 13px;
    color: #606266;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* 逻辑设计左上角的逻辑列表弹窗宽度 */
  .other-rule-list {
    width: 500px !important;
  }

  /* 应用中心, 移除每个分类末尾的透明方块(影响点击) */
  #app > div > div > div.jvs-layout.jvs-layout-tempOpen > div.template-content-box > div > div > div > div > div > div > img {
    display: none !important;
  }

  /* 应用中心星标:平时隐藏,悬停卡片浮现,已标记实心金星常驻;
     边框与定位由 :has() 驱动,只有注入了星标的卡片命中 */
  .application:has(.ze-star-btn) {
    position: relative !important;
    border: 1.5px solid transparent !important;
  }

  /* 特异性高于透明占位规则,只覆盖颜色,宽度样式继承上一条 */
  .application:has(.ze-star-btn.ze-marked) {
    border-color: #FAAD14 !important;
  }

  .ze-star-btn {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    background: transparent;
    line-height: 0;
    cursor: pointer;
    opacity: 0;
    color: #909399;
    transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1), color 0.15s cubic-bezier(0.4, 0, 0.2, 1), transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  }

  /* 覆盖 SVG 的 width/height 属性,按钮加大的同时星形同步加大 */
  .ze-star-btn svg {
    width: 18px;
    height: 18px;
  }

  .application:hover .ze-star-btn {
    opacity: 1;
  }

  /* 悬停星标本体:变金放大给出可点反馈;:active 同特异性靠后,按下回缩胜出 */
  .ze-star-btn:hover {
    color: #FAAD14;
    transform: scale(1.2);
  }

  .ze-star-btn:active {
    transform: scale(0.85);
  }

  .ze-star-btn.ze-marked {
    opacity: 1;
    color: #FAAD14;
  }

  /* fill 覆盖 svg 属性 fill="none" 实心化;fill 与 stroke 都走 currentColor 跟随 color */
  .ze-star-btn.ze-marked svg {
    fill: currentColor;
  }

  /* 只看星标过滤:开关状态挂在 body 上,隐藏非星标卡片与无星标部门组,
     同样由 :has() 驱动,JS 只产出 body class 这一个事实 */
  body.ze-star-filter-on :is(.application, .wrapper-content):not(:has(.ze-star-btn.ze-marked)) {
    display: none !important;
  }

  /* 开了过滤但一个星标都没有:提示行可见,其余规则把卡片全藏了。
     新版应用中心容器是 .app-page,旧版是 .template-content-box */
  body.ze-star-filter-on :is(.app-page, .template-content-box):not(:has(.ze-star-btn.ze-marked)) .ze-star-empty-tip {
    display: block;
  }

  /* 只看星标开关 pill:钉在应用列表上方,未激活灰描边、激活金色 */
  .ze-star-filter-btn {
    margin: 0 0 12px 12px;
    padding: 4px 12px;
    border: 1px solid #C0C4CC;
    border-radius: 12px;
    background: transparent;
    color: #909399;
    font-size: 12px;
    line-height: 20px;
    cursor: pointer;
    transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  }

  body.ze-star-filter-on .ze-star-filter-btn {
    border-color: #FAAD14;
    color: #FAAD14;
  }

  /* 旧版筛选行内(搜索框右侧):对齐与间距交给 flex 的 align-items/gap,清掉钉顶 margin */
  .filter-bar .ze-star-filter-btn {
    margin: 0;
  }

  /* 空态提示行:默认隐藏,仅空态规则放行 */
  .ze-star-empty-tip {
    display: none;
    margin: 24px 0;
    color: #909399;
    font-size: 14px;
    text-align: center;
  }

  /* 应用中心侧边栏收起:分类列与其右侧空白间隔列一起隐藏,卡片区拉满整行
     (el-col 为 float 布局、宽度全靠 class,覆盖需 !important) */
  body.ze-appcenter-side-collapsed :is(.sidebar-col, .sidebar-col + .el-col) {
    display: none !important;
  }

  body.ze-appcenter-side-collapsed .sidebar-col + .el-col + .el-col {
    width: 100% !important;
  }

  /* .app-page 自带 margin-left: -128px 的跨列偏移,侧边栏没了会推出屏幕外,一并归位 */
  body.ze-appcenter-side-collapsed .app-page {
    margin-left: 0 !important;
    width: 100% !important;
  }

  /* 侧边栏收起开关:仿应用内样式,白色圆角方块骑在侧边栏右缘,left 由 JS 校准 */
  .ze-side-toggle-btn {
    position: fixed;
    bottom: 48px;
    z-index: 999;
    width: 32px;
    height: 36px;
    padding: 0;
    border: none;
    border-radius: 10px;
    background: #fff;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    color: #606266;
    font-size: 16px;
    line-height: 36px;
    cursor: pointer;
    user-select: none;
  }

`;

GM_addStyle(JVS_STYLES + buildModeColorCss());
})();