Greasy Fork

Greasy Fork is available in English.

米家中枢极客版助手

登录极客版页面后,自动开启助手插件,显示设备、变量与自动化的关系,方便查找设备或变量用在了哪些自动化中。点击自动化规则名称即可跳转到自动化页面并高亮所对应的设备或变量卡片。支持快捷键折叠/展开,关闭,适应画布布局,设备高亮,日志高亮,自动适应画布、设置自动化列表布局样式等功能。

当前为 2025-09-26 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         米家中枢极客版助手
// @namespace    http://tampermonkey.net/
// @version      v0.9.12
// @description  登录极客版页面后,自动开启助手插件,显示设备、变量与自动化的关系,方便查找设备或变量用在了哪些自动化中。点击自动化规则名称即可跳转到自动化页面并高亮所对应的设备或变量卡片。支持快捷键折叠/展开,关闭,适应画布布局,设备高亮,日志高亮,自动适应画布、设置自动化列表布局样式等功能。
// @author       王丰,sk163
// @license      MIT
// @match        http://*/*
// @match        https://*/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==
/**
 * v0.9.12更新:
 * 1、修复多参数卡片未识别到变量的问题(感谢:零壹笃行反馈)
 *
 * v0.9.11更新:
 * 1、修复safari浏览器上显示异常的问题。(感谢:名字名字Air反馈)
 * 2、设置选项窗口增加了折叠后宽度、卡片颜色选择器等
 * 3、更换了整体显示风格。
 *
 * v0.9.10更新:
 * 1、通过查找设备、变量跳转到自动化页面后,将会自动画布大小,防止未高亮显示对应的设备卡片(感谢:追寻的风、间谍王师傅反馈)
 * 2、增加了虚拟事件的列表视图,可快速查找定位中枢发送或接收的虚拟事件自动化卡片。
 *
 * v0.9.9更新:
 * 1、支持https协议及端口访问(感谢:追寻的风、间谍王师傅反馈)
 *
 * v0.9.8更新:
 * 1、修复了变量列表可能重复的问题(感谢:间谍王师傅反馈)
 * 2、调整了一些显示逻辑的细节
 *
 * v0.9.7更新:
 * 1、修复自动化涉及的变量列表没有滚动条的问题(感谢:间谍王师傅反馈)
 *
 * v0.9.6更新:
 * 1、修复自动化涉及的设备列表没有滚动条的问题(感谢:间谍王师傅反馈)
 *
 * v0.9.5更新:
 * 1、自动化中设备和变量列表默认隐藏,可通过详情按钮进行展开。非常感谢原作者:【枋柚梓】PR的代码
 *
 * v0.9.4更新:
 * 1、修复变量可能引起的初始化错误
 *
 * v0.9.3更新:
 * 1、增加了在自动化中高亮显示指定的设备或变量名称
 * 2、修改了描述内容,[规则]的命名统一替换为了[自动化],方便理解
 * 3、修复其它已知问题
 *
 * v0.9.2更新:
 * 1、修复已知问题
 *
 * v0.9.1更新:
 * 1、增加了对变量视图的支持(变量类型、变量名称、变量值的筛选、变量排序、对应规则编排中的变量卡片高亮显示等)
 * 2、调整了选项布局
 * 3、支持了对表格文本的选择,方便复制
 * 注意:仅兼容极客版v1.6.0版本
 *
 * v0.9.0更新:
 * 1、适配v1.6.0极客版
 * 注意:仅兼容极客版v1.6.0版本
 *
 * v0.8.17更新:
 * 1、修正文本变量排序(作者:Derstood)
 *
 * v0.8.16更新:
 * 1、新增变量按照名称排序(作者:Derstood)
 *
 * v0.8.15更新:
 * 1、修复自动化列表在多列显示时,筛选格式混乱问题(作者:lqs1848)
 * 2、修复自动化列表在多列显示时,第一个自动化显示错位问题(作者:lqs1848)
 *
 * v0.8.14更新:
 * 1、增加刷新助手按钮,同时增加了快捷键Ctrl+R。查看所有的快捷键,可在极客版画布的【使用指南】中进行查看。
 * 2、优化了自动化规则列表在一行多列下的显示效果。
 * 3、修正了一些已知问题
 *
 * v0.8.13更新:
 * 1、修复了一行显示多列时,由于规则名称过长导致显示错位的问题
 *
 * v0.8.12更新:
 * 1、增加了快捷键Ctrl+W,关闭当前的画布
 * 2、将快捷键说明增加到了原生的【使用指南】中
 *
 * v0.8.11更新:
 * 1、规则列表换行显示
 *
 * v0.8.10更新:
 * 1、修复了窗口过小的问题
 * 2、取消了规则列表最小宽度限制
 *
 * v0.8.9更新:
 * 1、修改了日志高亮逻辑,优化了执行效率,提升了性能,减少无用的循环
 * 2、参考了米家自动化极客版样式优化(感谢原作者:lqs1848,http://greasyfork.icu/zh-CN/scripts/495833),增加了规则列表样式设置选项,可选择每行显示1-5条规则
 * 3、增加了自动折叠窗口的选项
 * 4、修正了一些已知问题
 *
 * v0.8.8更新:
 * 1. 更改了插件名称
 * 2、增加了快捷键,Ctrl/Command+E折叠/展开,Ctrl/Command+Q关闭,Ctrl/Command+B适应画布布局
 * 3、自动画布布局修改为仅在初次进入规则编排页面或激活编排页面时触发
 * 4、极客版登录后自动启动插件,无需再点击设备列表激活
 * 5、修正了一些已知问题
 */
(async () => {
    const callAPI = (api, params) => {
        return new Promise(res => editor.gateway.callAPI(api, params, res));
    };
    let scriptTitle = GM_info.script.name;
    let scriptVersion = GM_info.script.version;
    let isInit = false;
    let selectCardIds = '';
    let devMap = null;
    let defaultColor = '#43ad7f7f'
    let defaultWindowWidth = 1200;
    let defaultWindowHeight = 600;
    let defaultGraphWidth = 680;
    let defaultRuleStyle = '4';
    let minWindowWidth = 500;
    let minWindowHeight = 100;
    let minGraphWidth = 320;
    let enableEnhancedDisplayLog = GM_getValue("enableEnhancedDisplayLog");
    let enableAutoFitContent = GM_getValue("enableAutoFitContent");
    let enableAutoCollapseCheck = GM_getValue("enableAutoCollapseCheck");
    let backgroundColor = GM_getValue("backgroundColor");
    let windowWidth = GM_getValue("windowWidth");
    let windowHeight = GM_getValue("windowHeight");
    let graphWidth = GM_getValue("graphWidth");
    let ruleStyle = GM_getValue("ruleStyle");
    // 折叠状态标记(用于按钮切换)
    let isCollapsed = false;

    // 统一样式注入(Safari/Chrome 一致性)
    let styleInjected = false;

    function injectStyles() {
        if (styleInjected) return;
        styleInjected = true;
        const css = `
        :root {
            --mjk-font: -apple-system, MiSans, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
            --mjk-bg: #ffffff;
            --mjk-border: #d0d7de;
            --mjk-shadow: 0 8px 24px rgba(140,149,159,0.15);
            --mjk-radius: 8px;
            --mjk-pad: 10px;
            --mjk-z-topbar: 2147483646;
            --mjk-z-container: 2147483646;
            --mjk-z-graph: 2147483645;
            --mjk-z-options: 2147483647;
            --mjk-btn-bg: #f6f8fa;
            --mjk-btn-border: #d0d7de;
            --mjk-btn-hover: #eff1f3;
            --mjk-accent: #0000EE;
        }
        .mjk-container, .mjk-topbar, .mjk-options {
            font-family: var(--mjk-font);
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
        }
        .mjk-topbar {
            position: fixed;
            top: 0;
            right: 40px;
            width: var(--mjk-topbar-width, var(--mjk-width));
            height: 38px;
            background: var(--mjk-bg);
            z-index: var(--mjk-z-topbar);
            box-shadow: var(--mjk-shadow);
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 10px;
            border-radius: var(--mjk-radius);
            transform: translateZ(0);
        }
        .mjk-container {
            position: fixed;
            top: 10px;
            right: 40px;
            width: var(--mjk-width);
            height: var(--mjk-height);
            overflow: hidden;
            background: var(--mjk-bg);
            border: 1px solid var(--mjk-border);
            padding-top: 45px; /* 预留顶栏 */
            z-index: var(--mjk-z-container);
            box-shadow: var(--mjk-shadow);
            border-radius: var(--mjk-radius);
            transform: translateZ(0);
        }
        .mjk-options {
            position: fixed;
            top: 40px;
            right: 40px;
            width: 360px;
            min-height: 50px;
            display: flex;
            flex-direction: column;
            align-items: stretch;
            gap: 8px;
            padding: 12px;
            background: #f0f2f4;
            border: 1px solid var(--mjk-border);
            box-shadow: var(--mjk-shadow);
            z-index: var(--mjk-z-options);
            border-radius: var(--mjk-radius);
        }
        .mjk-btn {
            appearance: none;
            border: 1px solid var(--mjk-btn-border);
            background: var(--mjk-btn-bg);
            color: #24292f;
            height: 28px;
            padding: 0 10px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 12px;
            line-height: 26px;
        }
        .mjk-btn:hover { background: var(--mjk-btn-hover); }
        .mjk-switch { }
        .mjk-filter { }
        .mjk-input, .mjk-select {
            height: 28px;
            border: 1px solid var(--mjk-border);
            padding: 0 8px;
            border-radius: 6px;
            background: var(--mjk-bg);
        }
        .mjk-input { width: 200px; }
        .mjk-select {
            height: 32px;
            padding-right: 32px;
            line-height: 30px;
            cursor: pointer;
            appearance: none;
            -webkit-appearance: none;
            -moz-appearance: none;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23666' d='M6 8 0 0h12z'/%3E%3C/svg%3E");
            background-repeat: no-repeat;
            background-position: right 10px center;
            background-size: 12px 8px;
            transition: border-color 0.2s ease, box-shadow 0.2s ease;
        }
        .mjk-select:focus {
            outline: none;
            border-color: var(--mjk-accent);
            box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.15);
        }
        .mjk-select::-ms-expand { display: none; }
        .mjk-link-accent {
            color: var(--mjk-accent);
            text-decoration: none;
        }
        .mjk-link-accent:hover {
            text-decoration: underline;
        }
        .mjk-link-accent:active,
        .mjk-link-accent:focus,
        .mjk-link-accent.mjk-link-selected {
            color: #f5222d;
        }
        .mjk-color-picker {
            height: 28px;
            width: 44px;
            border: 1px solid var(--mjk-border);
            border-radius: 6px;
            padding: 0;
            background: transparent;
            cursor: pointer;
        }
        .mjk-table {
            width: 100%;
            border-collapse: collapse;
            user-select: text;
        }
        .mjk-table th, .mjk-table td {
            border: 1px solid var(--mjk-border);
            padding: 6px 8px;
            white-space: nowrap;
        }
        .mjk-table thead th {
            background: #f6f8fa;
            font-weight: 600;
        }
        .mjk-toolbar {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            margin-bottom: 8px;
        }
        .mjk-view-buttons { display: flex; gap: 8px; }
        #filtersSlot { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
        .mjk-opt-row { display: flex; align-items: center; gap: 10px; }
        #graph-list-container {
            position: fixed;
            top: 39px;
            right: 40px;
            background: var(--mjk-bg);
            border: 1px solid var(--mjk-border);
            box-shadow: var(--mjk-shadow);
            border-radius: var(--mjk-radius);
            z-index: var(--mjk-z-graph);
            height: calc(var(--mjk-height) - 50px);
            width: var(--mjk-graph-width, 680px);
        }
        /* 变量表格:更友好宽度与换行,让自动化列完整显示 */
        #varTable { table-layout: fixed; }
        #varTable th, #varTable td { white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
        #varTable th:nth-child(1), #varTable td:nth-child(1) { width: 80px; }
        #varTable th:nth-child(2), #varTable td:nth-child(2) { width: 80px; }
        #varTable th:nth-child(3), #varTable td:nth-child(3) { width: 140px; }
        #varTable th:nth-child(4), #varTable td:nth-child(4) { width: 300px; }
        #varTable th:nth-child(5), #varTable td:nth-child(5) { width: auto; }
        `;
        try {
            GM_addStyle(css);
        } catch (e) { /* Fallback */
            const style = document.createElement('style');
            style.textContent = css;
            document.head.appendChild(style);
        }
    }

    if (enableEnhancedDisplayLog === undefined || enableEnhancedDisplayLog === null || enableEnhancedDisplayLog === "") {
        enableEnhancedDisplayLog = true;
    }
    if (enableAutoFitContent === undefined || enableAutoFitContent === null || enableAutoFitContent === "") {
        enableAutoFitContent = true;
    }
    if (enableAutoCollapseCheck === undefined || enableAutoCollapseCheck === null || enableAutoCollapseCheck === "") {
        enableAutoCollapseCheck = false;
    }
    if (backgroundColor === undefined || backgroundColor === null || backgroundColor === "") {
        backgroundColor = defaultColor;
    }
    if (windowWidth === undefined || windowWidth === null || windowWidth === "" || isNaN(windowWidth)) {
        windowWidth = defaultWindowWidth;
    } else {
        const parsedWindowWidth = parseInt(windowWidth, 10);
        windowWidth = Number.isNaN(parsedWindowWidth) ? defaultWindowWidth : (parsedWindowWidth < minWindowWidth ? minWindowWidth : parsedWindowWidth);
    }
    if (windowHeight === undefined || windowHeight === null || windowHeight === "" || isNaN(windowHeight)) {
        windowHeight = defaultWindowHeight;
    } else {
        const parsedWindowHeight = parseInt(windowHeight, 10);
        windowHeight = Number.isNaN(parsedWindowHeight) ? defaultWindowHeight : (parsedWindowHeight < minWindowHeight ? minWindowHeight : parsedWindowHeight);
    }
    if (graphWidth === undefined || graphWidth === null || graphWidth === "" || isNaN(graphWidth)) {
        graphWidth = defaultGraphWidth;
    } else {
        const parsedGraphWidth = parseInt(graphWidth, 10);
        graphWidth = Number.isNaN(parsedGraphWidth) ? defaultGraphWidth : (parsedGraphWidth < minGraphWidth ? minGraphWidth : parsedGraphWidth);
    }
    if (ruleStyle === undefined || ruleStyle === null || ruleStyle === "") {
        ruleStyle = defaultRuleStyle;
    }

    // 将窗口尺寸同步到 CSS 变量
    function syncCSSDims() {
        try {
            document.documentElement.style.setProperty('--mjk-width', (parseInt(windowWidth, 10) || 0) + 'px');
            document.documentElement.style.setProperty('--mjk-height', (parseInt(windowHeight, 10) || 0) + 'px');
            document.documentElement.style.setProperty('--mjk-graph-width', (parseInt(graphWidth, 10) || 0) + 'px');
            document.documentElement.style.setProperty('--mjk-topbar-width', (parseInt(graphWidth, 10) || 0) + 'px');
        } catch (e) {
        }
    }

    const toPickerColor = (value) => {
        const normalized = typeof value === 'string' ? value.trim() : '';
        if (/^#([0-9a-fA-F]{6})$/.test(normalized)) {
            return normalized;
        }
        if (/^#([0-9a-fA-F]{8})$/.test(normalized)) {
            return normalized.slice(0, 7);
        }
        const fallback = typeof defaultColor === 'string' ? defaultColor.trim() : '';
        if (/^#([0-9a-fA-F]{6})$/.test(fallback)) {
            return fallback;
        }
        if (/^#([0-9a-fA-F]{8})$/.test(fallback)) {
            return fallback.slice(0, 7);
        }
        return '#ffffff';
    };

    const bindLinkClickHighlight = (link) => {
        if (!link) return;
        const activate = () => {
            document.querySelectorAll('.mjk-link-accent.mjk-link-selected').forEach(node => {
                if (node !== link) {
                    node.classList.remove('mjk-link-selected');
                }
            });
            link.classList.add('mjk-link-selected');
        };
        link.addEventListener('mousedown', activate);
        link.addEventListener('click', activate);
        link.addEventListener('focus', activate);
    };

    const executeScript = async () => {
        if (document.getElementById('device-rule-map') || isInit === true) {
            return;
        }

        if (typeof editor === 'undefined' || typeof editor.gateway === 'undefined' || typeof editor.gateway.callAPI === 'undefined') {
            console.error('editor.gateway.callAPI 方法未定义。请确保在正确的环境中运行此脚本。');
            return;
        }

        try {
            injectStyles();
            syncCSSDims();
            isInit = true;
            const devListResponse = await callAPI('getDevList');
            devMap = devListResponse.devList;
            const roomNames = Array.from(new Set(Object.values(devMap).map(device => device.roomName)));
            let varRuleMap = {};
            let devRuleMap = {};
            let varMap = {};
            let sendEventRuleMap = {};
            let receiveEventRuleMap = {};

            const varScopes = (await callAPI('getVarScopeList', {})).scopes;
            for (const scope of varScopes) {
                const vars = await callAPI('getVarList', {scope: scope});
                Object.entries(vars).forEach(([vid, v]) => {
                    varRuleMap[vid] = [];
                    varMap[vid] = {
                        name: v.userData.name,
                        scope: (scope === "global" ? "全局" : "局部"),
                        type: (v.type === "string" ? "文本" : "数值"),
                        value: v.value
                    }
                });
            }

            const ruleList = await callAPI('getGraphList')
            for (const rule of ruleList) {
                const content = await callAPI('getGraph', {id: rule.id});
                const dids = new Set(content.nodes.map(n => n.props?.did).filter(did => did !== undefined));
                const devCards = new Set(content.nodes.map(n => {
                    return (n.props && n.id) ? {did: n.props.did, cid: n.id} : undefined;
                }).filter(card => card !== undefined));

                dids.forEach(did => {
                    devRuleMap[did] = devRuleMap[did] ?? [];
                    const cardIds = Array.from(devCards)
                        .filter(card => card.did === did)
                        .map(card => card.cid).join(',');
                    const tempDevRule = {
                        ruleId: rule.id,
                        cardIds: cardIds,
                        totalCardNum: devCards.size
                    };

                    devRuleMap[did].push(tempDevRule);
                });

                const varCards = content.nodes.flatMap(node => {
                    if (!node.props || !node.id) return [];

                    const cards = [];
                    // 直接变量卡片
                    if (node.props.scope) {
                        cards.push({vid: node.props.id, cid: node.id});
                    }
                    // 参数中的变量
                    if (node.props.arguments?.length > 0) {
                        cards.push(...node.props.arguments
                            .filter(arg => arg.id)
                            .map(arg => ({vid: arg.id, cid: node.id}))
                        );
                    }
                    return cards;
                });

                const varids = new Set(content.nodes
                    .flatMap(node => {
                        if (!node.props) return [];

                        const ids = [];
                        // 直接变量卡片的ID
                        if (node.props.scope) {
                            ids.push(node.props.id);
                        }
                        // 参数中的变量ID
                        if (node.props.arguments?.length > 0) {
                            ids.push(...node.props.arguments
                                .filter(arg => arg.id)
                                .map(arg => arg.id)
                            );
                        }
                        return ids;
                    })
                );

                varids.forEach(vid => {
                    varRuleMap[vid] = varRuleMap[vid] ?? [];
                    const cardIds = varCards
                        .filter(card => card.vid === vid)
                        .map(card => card.cid)
                        .join(',');
                    varRuleMap[vid].push({
                        ruleId: rule.id,
                        cardIds: cardIds,
                        totalCardNum: varCards.length
                    });
                });

                //发出虚拟事件的列表
                const gatewaySendEventCards = new Set(content.nodes.map(n => {
                    return (n.props && n.cfg && n.id && n.type === "deviceOutput"
                        && n.cfg.urn && n.cfg.urn.indexOf("device:gateway") > -1
                        && n.props.siid && n.props.siid === 4 && n.props.aiid && n.props.aiid === 1
                        && n.props.ins && n.props.ins[0] && n.props.ins[0].piid && n.props.ins[0].piid === 1
                    ) ? {
                        did: n.props.did,
                        cid: n.id,
                        eventName: n.props.ins[0].value ? n.props.ins[0].value : n.props.ins[0].id + "$$" + n.props.ins[0].scope
                    } : undefined;
                }).filter(card => card !== undefined));
                //接收虚拟事件的列表
                const gatewayReceiveEventCards = new Set(content.nodes.map(n => {
                    return (n.props && n.cfg && n.id && n.type === "deviceInput"
                        && n.cfg.urn && n.cfg.urn.indexOf("device:gateway") > -1
                        && n.props.siid && n.props.siid === 4 && n.props.eiid && n.props.eiid === 1
                        && n.props.arguments && n.props.arguments[0] && n.props.arguments[0].piid && n.props.arguments[0].piid === 1
                    ) ? {did: n.props.did, cid: n.id, eventName: n.props.arguments[0].v1} : undefined;
                }).filter(card => card !== undefined));

                const processEventCards = (eventCards, eventType) => {
                    eventCards.forEach(event => {
                        let eventName = event.eventName;
                        let cid = event.cid;
                        if (eventName.indexOf("$$") > -1) {
                            let vid = event.eventName.split("$$")[0];
                            let scope = event.eventName.split("$$")[1];
                            const varData = varMap[vid];
                            if (varData) {
                                eventName = "变量名:${" + varData.name + "},当前值:" + varData.value;
                            }
                        }
                        if (eventType === "send") {
                            sendEventRuleMap[eventName] = sendEventRuleMap[eventName] ?? {};
                            sendEventRuleMap[eventName][rule.id] = sendEventRuleMap[eventName][rule.id] ?? {}
                            sendEventRuleMap[eventName][rule.id]["totalCardNum"] = eventCards.size;
                            sendEventRuleMap[eventName][rule.id]["cardIds"] = sendEventRuleMap[eventName][rule.id]["cardIds"] ?? []
                            sendEventRuleMap[eventName][rule.id]["cardIds"].push(cid);
                        } else {
                            receiveEventRuleMap[eventName] = receiveEventRuleMap[eventName] ?? [];
                            receiveEventRuleMap[eventName][rule.id] = receiveEventRuleMap[eventName][rule.id] ?? {}
                            receiveEventRuleMap[eventName][rule.id]["totalCardNum"] = eventCards.size;
                            receiveEventRuleMap[eventName][rule.id]["cardIds"] = receiveEventRuleMap[eventName][rule.id]["cardIds"] ?? []
                            receiveEventRuleMap[eventName][rule.id]["cardIds"].push(cid);
                        }
                    });
                };

                processEventCards(gatewaySendEventCards, "send");
                processEventCards(gatewayReceiveEventCards, "receive");
            }
            const devRuleData = Object.fromEntries(
                Object.entries(devRuleMap).map(([did, devRules]) => [
                    did,
                    {
                        device: {
                            name: devMap[did]?.name ?? `did: ${did}`,
                            roomName: devMap[did]?.roomName ?? `未知`
                        },
                        rules: devRules.map(dr => {
                            const rule = ruleList.find(r => r.id === dr.ruleId);
                            return {
                                id: dr.ruleId,
                                cardIds: dr.cardIds,
                                totalCardNum: dr.totalCardNum,
                                name: rule ? rule.userData.name : 'Unknown'
                            };
                        })
                    }
                ])
            );

            const varRuleData = Object.fromEntries(
                Object.entries(varRuleMap).map(([vid, varRules]) => [
                    vid,
                    {
                        rules: varRules.map(vr => {
                            const rule = ruleList.find(r => r.id === vr.ruleId);
                            return {
                                id: vr.ruleId,
                                cardIds: vr.cardIds,
                                totalCardNum: vr.totalCardNum,
                                name: rule ? rule.userData.name : 'Unknown'
                            };
                        })
                    }
                ])
            );

            const sendEventRuleData = Object.fromEntries(
                Object.entries(sendEventRuleMap).map(([eventName, eventRuleCardsMap]) => [
                    eventName,
                    {
                        rules: Object.entries(eventRuleCardsMap).map(([ruleId, cardsInfo]) => {
                            const rule = ruleList.find(r => r.id === ruleId);
                            return {
                                id: ruleId,
                                cardIds: cardsInfo["cardIds"].join(','),
                                totalCardNum: cardsInfo["totalCardNum"],
                                name: rule ? rule.userData.name : 'Unknown'
                            };
                        })
                    }
                ])
            );
            const receiveEventRuleData = Object.fromEntries(
                Object.entries(receiveEventRuleMap).map(([eventName, eventRuleCardsMap]) => [
                    eventName,
                    {
                        rules: Object.entries(eventRuleCardsMap).map(([ruleId, cardsInfo]) => {
                            const rule = ruleList.find(r => r.id === ruleId);
                            return {
                                id: ruleId,
                                cardIds: cardsInfo["cardIds"].join(','),
                                totalCardNum: cardsInfo["totalCardNum"],
                                name: rule ? rule.userData.name : 'Unknown'
                            };
                        })
                    }
                ])
            );

            const container = document.createElement('div');
            container.id = 'device-rule-map';
            container.classList.add('mjk-container');
            container.style.position = 'fixed';
            container.style.top = '10px';
            container.style.right = '40px';
            // 尺寸由 CSS 变量控制:--mjk-width / --mjk-height
            container.style.overflowY = 'scroll';
            container.style.backgroundColor = 'white';
            container.style.border = '1px solid #ccc';
            container.style.paddingTop = '45px';
            container.style.zIndex = 2147483646; // 提升层级,避免被页面浮层遮挡(Safari 兼容)
            container.style.transform = 'translateZ(0)'; // 触发硬件加速,规避 Safari 偶发不绘制
            container.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';

            const topBar = document.createElement('div');
            topBar.id = 'topBar';
            topBar.classList.add('mjk-topbar');
            topBar.style.position = 'fixed';
            topBar.style.top = '0';
            topBar.style.right = '40px';
            // 顶栏宽度由 CSS 变量控制
            topBar.style.height = '38px';
            topBar.style.backgroundColor = 'white';
            topBar.style.zIndex = 2147483646; // 顶栏次于选项层
            topBar.style.transform = 'translateZ(0)';
            topBar.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
            topBar.style.display = 'flex';
            topBar.style.justifyContent = 'space-between';
            topBar.style.alignItems = 'center';
            topBar.style.padding = '0 10px';

            const titleDiv = document.createElement('div');
            titleDiv.style.display = 'flex';
            const title = document.createElement('h2');
            title.style.margin = '0';
            title.textContent = scriptTitle;
            titleDiv.appendChild(title);
            const version = document.createElement('span');
            version.textContent = scriptVersion;
            version.style.marginLeft = '2px';
            version.style.paddingTop = '16px';
            version.style.fontSize = '9px';
            titleDiv.appendChild(version);

            const buttonContainer = document.createElement('div');
            buttonContainer.style.display = 'flex';
            buttonContainer.style.gap = '10px';

            const optionsButton = document.createElement('button');
            optionsButton.id = "optionsButton";
            optionsButton.className = 'mjk-btn';
            optionsButton.textContent = '选项';
            optionsButton.title = '选项';
            optionsButton.onclick = () => {
                handleOptionsBtnClick();
            };

            const collapseButton = document.createElement('button');
            collapseButton.id = "collapseButton";
            collapseButton.className = 'mjk-btn';
            collapseButton.textContent = '折叠';
            collapseButton.title = '快捷键为Ctrl+E';
            collapseButton.onclick = () => {
                handleCollapseBtnClick();
            };

            const closeButton = document.createElement('button');
            closeButton.id = "closeButton";
            closeButton.className = 'mjk-btn';
            closeButton.title = '快捷键为Ctrl+Q';
            closeButton.textContent = '关闭';
            closeButton.onclick = () => {
                closeGraphContainer();
                if (document.getElementById('device-rule-map')) {
                    document.body.removeChild(container);
                }
                const oldTopBar = document.getElementById('topBar');
                if (oldTopBar && oldTopBar.parentElement) {
                    oldTopBar.parentElement.removeChild(oldTopBar);
                }
                const oldOptions = document.getElementById('optionsContainer');
                if (oldOptions && oldOptions.parentElement) {
                    oldOptions.parentElement.removeChild(oldOptions);
                }
                isInit = false;
            }

            const refreshButton = document.createElement('button');
            refreshButton.id = "refreshButton";
            refreshButton.className = 'mjk-btn';
            refreshButton.title = '快捷键为Ctrl+R';
            refreshButton.textContent = '刷新';
            refreshButton.onclick = () => {
                closeGraphContainer();
                if (document.getElementById('device-rule-map')) {
                    document.body.removeChild(container);
                }
                const oldTopBar = document.getElementById('topBar');
                if (oldTopBar && oldTopBar.parentElement) {
                    oldTopBar.parentElement.removeChild(oldTopBar);
                }
                const oldOptions = document.getElementById('optionsContainer');
                if (oldOptions && oldOptions.parentElement) {
                    oldOptions.parentElement.removeChild(oldOptions);
                }
                isInit = false;
                executeScript();
                setTimeout(function () {
                    handleUrlChange();
                }, 500);
            }

            const graphListDetailButton = document.createElement('button');
            graphListDetailButton.id = "graphListDetailButton";
            graphListDetailButton.className = 'mjk-btn';
            graphListDetailButton.textContent = '详情';
            graphListDetailButton.style.display = 'none';
            graphListDetailButton.onclick = () => {
                handleGraphListDetailBtnClick();
            }

            buttonContainer.appendChild(collapseButton);
            buttonContainer.appendChild(graphListDetailButton);
            buttonContainer.appendChild(optionsButton);
            buttonContainer.appendChild(refreshButton);
            buttonContainer.appendChild(closeButton);

            topBar.appendChild(titleDiv);
            topBar.appendChild(buttonContainer);

            const optionsContainer = document.createElement('div');
            optionsContainer.id = 'optionsContainer';
            optionsContainer.classList.add('mjk-options');
            optionsContainer.style.display = 'none';

            const widthInput = document.createElement('input');
            widthInput.className = 'mjk-input';
            widthInput.type = 'text';
            widthInput.value = windowWidth;
            widthInput.placeholder = windowWidth + 'px';
            widthInput.onchange = () => {
                const parsedWidth = parseInt(widthInput.value, 10);
                if (Number.isNaN(parsedWidth)) {
                    widthInput.value = windowWidth;
                    return;
                }
                windowWidth = parsedWidth < minWindowWidth ? minWindowWidth : parsedWidth;
                widthInput.value = windowWidth;
                GM_setValue("windowWidth", windowWidth);
                syncCSSDims();
            };
            const spanW = document.createElement('span');
            spanW.textContent = '主窗口宽度:';
            spanW.style.marginLeft = '10px';
            const rowW = document.createElement('div');
            rowW.className = 'mjk-opt-row';
            rowW.appendChild(spanW);
            rowW.appendChild(widthInput);
            optionsContainer.appendChild(rowW);

            const heightInput = document.createElement('input');
            heightInput.className = 'mjk-input';
            heightInput.type = 'text';
            heightInput.value = windowHeight;
            heightInput.placeholder = windowHeight + 'px';
            heightInput.onchange = () => {
                const parsedHeight = parseInt(heightInput.value, 10);
                if (Number.isNaN(parsedHeight)) {
                    heightInput.value = windowHeight;
                    return;
                }
                windowHeight = parsedHeight < minWindowHeight ? minWindowHeight : parsedHeight;
                heightInput.value = windowHeight;
                GM_setValue("windowHeight", windowHeight);
                syncCSSDims();
            };
            const spanH = document.createElement('span');
            spanH.textContent = '主窗口高度:';
            spanH.style.marginLeft = '10px';
            const rowH = document.createElement('div');
            rowH.className = 'mjk-opt-row';
            rowH.appendChild(spanH);
            rowH.appendChild(heightInput);
            optionsContainer.appendChild(rowH);

            const graphWidthInput = document.createElement('input');
            graphWidthInput.className = 'mjk-input';
            graphWidthInput.type = 'text';
            graphWidthInput.value = graphWidth;
            graphWidthInput.placeholder = graphWidth + 'px';
            graphWidthInput.onchange = () => {
                const parsedGraphWidthInput = parseInt(graphWidthInput.value, 10);
                if (Number.isNaN(parsedGraphWidthInput)) {
                    graphWidthInput.value = graphWidth;
                    return;
                }
                graphWidth = parsedGraphWidthInput < minGraphWidth ? minGraphWidth : parsedGraphWidthInput;
                graphWidthInput.value = graphWidth;
                GM_setValue("graphWidth", graphWidth);
                syncCSSDims();
            };
            const spanGraph = document.createElement('span');
            spanGraph.textContent = '折叠后宽度:';
            spanGraph.style.marginLeft = '10px';
            const rowGraph = document.createElement('div');
            rowGraph.className = 'mjk-opt-row';
            rowGraph.appendChild(spanGraph);
            rowGraph.appendChild(graphWidthInput);
            optionsContainer.appendChild(rowGraph);

            const ruleStyleSelect = document.createElement('select');
            ruleStyleSelect.className = 'mjk-select';
            ruleStyleSelect.innerHTML = '<option value="1">每行1列</option>' +
                '<option value="2">每行2列</option>' +
                '<option value="3">每行3列</option>' +
                '<option value="4">每行4列</option>' +
                '<option value="5">每行5列</option>';
            ruleStyleSelect.onchange = () => {
                GM_setValue("ruleStyle", ruleStyleSelect.value);
                changeRuleListStyle(ruleStyleSelect.value);
            };
            const spanS = document.createElement('span');
            spanS.textContent = '自动化列表:';
            spanS.style.marginLeft = '10px';
            const rowS = document.createElement('div');
            rowS.className = 'mjk-opt-row';
            rowS.appendChild(spanS);
            rowS.appendChild(ruleStyleSelect);
            optionsContainer.appendChild(rowS);
            ruleStyleSelect.value = ruleStyle;
            changeRuleListStyle(ruleStyle);

            const colorInput = document.createElement('input');
            colorInput.className = 'mjk-input';
            colorInput.type = 'text';
            colorInput.placeholder = defaultColor;
            colorInput.style.width = '120px';
            colorInput.value = backgroundColor;
            const colorPicker = document.createElement('input');
            colorPicker.type = 'color';
            colorPicker.className = 'mjk-color-picker';
            colorPicker.value = toPickerColor(backgroundColor || defaultColor);
            colorInput.oninput = () => {
                backgroundColor = colorInput.value;
                GM_setValue("backgroundColor", backgroundColor);
                const pickerValue = toPickerColor(backgroundColor);
                if (pickerValue) {
                    colorPicker.value = pickerValue;
                }
            };
            colorPicker.oninput = () => {
                const currentValue = colorInput.value.trim();
                const alphaSuffix = /^#([0-9a-fA-F]{8})$/.test(currentValue) ? currentValue.slice(7) : '';
                colorInput.value = colorPicker.value + alphaSuffix;
                colorInput.dispatchEvent(new Event('input', {bubbles: true}));
            };
            const spanC = document.createElement('span');
            spanC.textContent = '卡片颜色:';
            spanC.style.marginLeft = '10px';
            const rowC = document.createElement('div');
            rowC.className = 'mjk-opt-row';
            rowC.appendChild(spanC);
            rowC.appendChild(colorInput);
            rowC.appendChild(colorPicker);
            optionsContainer.appendChild(rowC);

            const logLabel = document.createElement('label');
            logLabel.htmlFor = 'highlightLogCheck';
            logLabel.appendChild(document.createTextNode('日志高亮'));
            logLabel.style.marginBottom = '10px';
            logLabel.style.marginLeft = '10px';
            const rowLog = document.createElement('div');
            rowLog.className = 'mjk-opt-row';
            rowLog.appendChild(logLabel);

            const highlightLogCheck = document.createElement('input');
            highlightLogCheck.className = 'mjk-checkbox';
            highlightLogCheck.type = 'checkbox';
            highlightLogCheck.id = 'highlightLogCheck';
            highlightLogCheck.checked = enableEnhancedDisplayLog;
            highlightLogCheck.style.marginLeft = '2px';
            highlightLogCheck.onchange = function () {
                enableEnhancedDisplayLog = highlightLogCheck.checked;
                GM_setValue("enableEnhancedDisplayLog", enableEnhancedDisplayLog);
            };
            rowLog.appendChild(highlightLogCheck);
            optionsContainer.appendChild(rowLog);

            const fitLabel = document.createElement('label');
            fitLabel.htmlFor = 'autoFitCheck';
            fitLabel.appendChild(document.createTextNode('自动画布'));
            fitLabel.style.marginBottom = '10px';
            fitLabel.style.marginLeft = '10px';
            const rowFit = document.createElement('div');
            rowFit.className = 'mjk-opt-row';
            rowFit.appendChild(fitLabel);

            const autoFitCheck = document.createElement('input');
            autoFitCheck.className = 'mjk-checkbox';
            autoFitCheck.type = 'checkbox';
            autoFitCheck.id = 'autoFitCheck';
            autoFitCheck.checked = enableAutoFitContent;
            autoFitCheck.style.marginLeft = '2px';
            autoFitCheck.onchange = function () {
                enableAutoFitContent = autoFitCheck.checked;
                GM_setValue("enableAutoFitContent", enableAutoFitContent);
            };
            rowFit.appendChild(autoFitCheck);
            optionsContainer.appendChild(rowFit);

            const autoCollapseLabel = document.createElement('label');
            autoCollapseLabel.htmlFor = 'autoCollapseCheck';
            autoCollapseLabel.appendChild(document.createTextNode('自动折叠'));
            autoCollapseLabel.style.marginBottom = '10px';
            autoCollapseLabel.style.marginLeft = '10px';
            const rowCollapse = document.createElement('div');
            rowCollapse.className = 'mjk-opt-row';
            rowCollapse.appendChild(autoCollapseLabel);
            const autoCollapseCheck = document.createElement('input');
            autoCollapseCheck.className = 'mjk-checkbox';
            autoCollapseCheck.type = 'checkbox';
            autoCollapseCheck.id = 'autoCollapseCheck';
            autoCollapseCheck.checked = enableAutoCollapseCheck;
            autoCollapseCheck.style.marginLeft = '2px';
            autoCollapseCheck.onchange = function () {
                enableAutoCollapseCheck = autoCollapseCheck.checked;
                GM_setValue("enableAutoCollapseCheck", enableAutoCollapseCheck);
                autoCollapse();
            };
            rowCollapse.appendChild(autoCollapseCheck);
            optionsContainer.appendChild(rowCollapse);
            // 选项窗口固定到 body,避免受容器折叠与 transform 影响(Safari 兼容)
            const existedOptions = document.getElementById('optionsContainer');
            if (existedOptions && existedOptions !== optionsContainer && existedOptions.parentElement) {
                existedOptions.parentElement.removeChild(existedOptions);
            }
            document.body.appendChild(optionsContainer);


            //设备表格
            const devTable = document.createElement('table');
            devTable.id = 'devTable';
            devTable.className = 'mjk-table';
            devTable.border = '1';
            devTable.cellSpacing = '0';
            devTable.cellPadding = '5';
            devTable.style.width = '100%';
            devTable.style.border = '1px soild black';
            devTable.style.userSelect = 'text';
            devTable.style.borderCollapse = 'collapse';

            const devThead = document.createElement('thead');
            const devHeaderRow = document.createElement('tr');
            const devRoomHeader = document.createElement('th');
            const devDeviceHeader = document.createElement('th');
            const devRuleHeader = document.createElement('th');
            const devTbody = document.createElement('tbody');

            let roomSortOrder = 'asc';
            let deviceSortOrder = 'asc';
            let ruleSortOrder = 'asc';

            const devUpdateSortMarkers = () => {
                devRoomHeader.innerHTML = `房间 ${roomSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                devDeviceHeader.innerHTML = `设备 ${deviceSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                devRuleHeader.innerHTML = `自动化名称 ${ruleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
            };

            devRoomHeader.textContent = '房间';
            devRoomHeader.style.textWrap = 'nowrap';
            devDeviceHeader.textContent = '设备';
            devDeviceHeader.style.textWrap = 'nowrap';
            devRuleHeader.textContent = '自动化名称';

            devRoomHeader.onclick = () => {
                roomSortOrder = roomSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(devTbody, 0, roomSortOrder);
                devUpdateSortMarkers();
            };
            devDeviceHeader.onclick = () => {
                deviceSortOrder = deviceSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(devTbody, 1, deviceSortOrder);
                devUpdateSortMarkers();
            };
            devRuleHeader.onclick = () => {
                ruleSortOrder = ruleSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(devTbody, 2, ruleSortOrder);
                devUpdateSortMarkers();
            };

            devHeaderRow.appendChild(devRoomHeader);
            devHeaderRow.appendChild(devDeviceHeader);
            devHeaderRow.appendChild(devRuleHeader);
            devThead.appendChild(devHeaderRow);
            devTable.appendChild(devThead);

            const devFilterContainer = document.createElement('span');
            devFilterContainer.id = 'devFilterContainer';
            devFilterContainer.classList.add('mjk-filter');
            devFilterContainer.style.display = '';

            const roomFilterSelect = document.createElement('select');
            roomFilterSelect.className = 'mjk-select';
            roomFilterSelect.style.height = '32px';
            roomFilterSelect.style.borderStyle = 'solid';
            roomFilterSelect.style.borderWidth = '1px';
            roomFilterSelect.innerHTML = `<option value="">所有房间</option>` + roomNames.map(room => `<option value="${room}">${room}</option>`).join('');
            roomFilterSelect.onchange = () => {
                filterDevTable(roomFilterSelect.value, deviceFilterInput.value, devRuleFilterInput.value);
            };
            const deviceFilterInput = document.createElement('input');
            deviceFilterInput.className = 'mjk-input';
            deviceFilterInput.type = 'text';
            deviceFilterInput.placeholder = '设备筛选';
            deviceFilterInput.style.width = '200px';
            deviceFilterInput.style.marginLeft = '10px';
            deviceFilterInput.style.height = '28px';
            deviceFilterInput.style.borderStyle = 'solid';
            deviceFilterInput.style.borderWidth = '1px';
            deviceFilterInput.oninput = () => {
                filterDevTable(roomFilterSelect.value, deviceFilterInput.value, devRuleFilterInput.value);
            };
            const devRuleFilterInput = document.createElement('input');
            devRuleFilterInput.className = 'mjk-input';
            devRuleFilterInput.type = 'text';
            devRuleFilterInput.placeholder = '自动化名称筛选';
            devRuleFilterInput.style.width = '200px';
            devRuleFilterInput.style.marginLeft = '10px';
            devRuleFilterInput.style.height = '28px';
            devRuleFilterInput.style.borderStyle = 'solid';
            devRuleFilterInput.style.borderWidth = '1px';
            devRuleFilterInput.oninput = () => {
                filterDevTable(roomFilterSelect.value, deviceFilterInput.value, devRuleFilterInput.value);
            };

            devFilterContainer.appendChild(roomFilterSelect);
            devFilterContainer.appendChild(deviceFilterInput);
            devFilterContainer.appendChild(devRuleFilterInput);

            // 工具栏(左侧过滤 + 右侧视图按钮)
            const toolbarRow = document.createElement('div');
            toolbarRow.id = 'toolbarRow';
            toolbarRow.className = 'mjk-toolbar';
            const filtersSlot = document.createElement('div');
            filtersSlot.id = 'filtersSlot';
            toolbarRow.appendChild(filtersSlot);
            const viewButtons = document.createElement('div');
            viewButtons.id = 'viewButtons';
            viewButtons.className = 'mjk-view-buttons';
            toolbarRow.appendChild(viewButtons);
            container.appendChild(toolbarRow);

            const varFilterContainer = document.createElement('span');
            varFilterContainer.id = 'varFilterContainer';
            varFilterContainer.classList.add('mjk-filter');
            varFilterContainer.style.display = 'none';

            const varScopeFilterSelect = document.createElement('select');
            varScopeFilterSelect.className = 'mjk-select';
            varScopeFilterSelect.innerHTML =
                '<option value="">所有变量范围</option>' +
                '<option value="全局">全局</option>' +
                '<option value="局部">局部</option>';
            varScopeFilterSelect.onchange = () => {
                filterVarTable(varScopeFilterSelect.value, varTypeFilterSelect.value, varNameFilterInput.value, varValueFilterInput.value, varRuleFilterInput.value);
            };
            const varTypeFilterSelect = document.createElement('select');
            varTypeFilterSelect.className = 'mjk-select';
            varTypeFilterSelect.innerHTML =
                '<option value="">所有变量类型</option>' +
                '<option value="文本">文本</option>' +
                '<option value="数值">数值</option>';
            varTypeFilterSelect.onchange = () => {
                filterVarTable(varScopeFilterSelect.value, varTypeFilterSelect.value, varNameFilterInput.value, varValueFilterInput.value, varRuleFilterInput.value);
            };
            const varNameFilterInput = document.createElement('input');
            varNameFilterInput.className = 'mjk-input';
            varNameFilterInput.type = 'text';
            varNameFilterInput.placeholder = '变量名称筛选';
            varNameFilterInput.oninput = () => {
                filterVarTable(varScopeFilterSelect.value, varTypeFilterSelect.value, varNameFilterInput.value, varValueFilterInput.value, varRuleFilterInput.value);
            };

            const varValueFilterInput = document.createElement('input');
            varValueFilterInput.className = 'mjk-input';
            varValueFilterInput.type = 'text';
            varValueFilterInput.placeholder = '变量值称筛选';
            varValueFilterInput.oninput = () => {
                filterVarTable(varScopeFilterSelect.value, varTypeFilterSelect.value, varNameFilterInput.value, varValueFilterInput.value, varRuleFilterInput.value);
            };
            const varRuleFilterInput = document.createElement('input');
            varRuleFilterInput.className = 'mjk-input';
            varRuleFilterInput.type = 'text';
            varRuleFilterInput.placeholder = '自动化名称筛选';
            varRuleFilterInput.oninput = () => {
                filterVarTable(varScopeFilterSelect.value, varTypeFilterSelect.value, varNameFilterInput.value, varValueFilterInput.value, varRuleFilterInput.value);
            };
            varFilterContainer.appendChild(varScopeFilterSelect);
            varFilterContainer.appendChild(varTypeFilterSelect);
            varFilterContainer.appendChild(varNameFilterInput);
            varFilterContainer.appendChild(varValueFilterInput);
            varFilterContainer.appendChild(varRuleFilterInput);
            // 过滤区放入工具栏左侧插槽(初始为设备过滤)
            filtersSlot.appendChild(devFilterContainer);
            filtersSlot.appendChild(varFilterContainer);

            const eventFilterContainer = document.createElement('span');
            eventFilterContainer.id = 'eventFilterContainer';
            eventFilterContainer.classList.add('mjk-filter');
            eventFilterContainer.style.display = 'none';
            const eventTypeFilterSelect = document.createElement('select');
            eventTypeFilterSelect.className = 'mjk-select';
            eventTypeFilterSelect.innerHTML =
                '<option value="">虚拟事件类型</option>' +
                '<option value="发送">发送</option>' +
                '<option value="接收">接收</option>';
            eventTypeFilterSelect.onchange = () => {
                filterEventTable(eventTypeFilterSelect.value, eventNameFilterInput.value, eventRuleFilterInput.value);
            };

            const eventNameFilterInput = document.createElement('input');
            eventNameFilterInput.className = 'mjk-input';
            eventNameFilterInput.type = 'text';
            eventNameFilterInput.placeholder = '虚拟事件名称筛选';
            eventNameFilterInput.oninput = () => {
                filterEventTable(eventTypeFilterSelect.value, eventNameFilterInput.value, eventRuleFilterInput.value);
            };
            const eventRuleFilterInput = document.createElement('input');
            eventRuleFilterInput.className = 'mjk-input';
            eventRuleFilterInput.type = 'text';
            eventRuleFilterInput.placeholder = '自动化名称筛选';
            eventRuleFilterInput.oninput = () => {
                filterEventTable(eventTypeFilterSelect.value, eventNameFilterInput.value, eventRuleFilterInput.value);
            };

            eventFilterContainer.appendChild(eventTypeFilterSelect);
            eventFilterContainer.appendChild(eventNameFilterInput);
            eventFilterContainer.appendChild(eventRuleFilterInput);
            filtersSlot.appendChild(eventFilterContainer);


            const changeDevButton = document.createElement('button');
            changeDevButton.id = "changeDevButton";
            changeDevButton.className = 'mjk-btn mjk-switch';
            changeDevButton.textContent = '设备列表';
            changeDevButton.title = '切换至设备列表视图';
            changeDevButton.onclick = () => {
                handleChangeViewBtnClick("dev");
            };
            viewButtons.appendChild(changeDevButton);

            const changeVarButton = document.createElement('button');
            changeVarButton.id = "changeVarButton";
            changeVarButton.className = 'mjk-btn mjk-switch';
            changeVarButton.textContent = '变量列表';
            changeVarButton.title = '切换至变量列表视图';
            changeVarButton.onclick = () => {
                handleChangeViewBtnClick("var");
            };
            viewButtons.appendChild(changeVarButton);

            const changeEventButton = document.createElement('button');
            changeEventButton.id = "changeEventButton";
            changeEventButton.className = 'mjk-btn mjk-switch';
            changeEventButton.textContent = '虚拟事件';
            changeEventButton.title = '切换至发送虚拟事件视图';
            changeEventButton.onclick = () => {
                handleChangeViewBtnClick("event");
            };
            viewButtons.appendChild(changeEventButton);

            Object.entries(devRuleData).forEach(([did, data]) => {
                const device = data.device;
                const rules = data.rules;
                const row = document.createElement('tr');
                const roomCell = document.createElement('td');
                roomCell.textContent = device.roomName;
                roomCell.style.textWrap = 'nowrap';
                const deviceCell = document.createElement('td');
                deviceCell.textContent = device.name;
                deviceCell.style.textWrap = 'nowrap';
                const ruleCell = document.createElement('td');

                const protocol = window.location.protocol;
                const host = window.location.host;
                let sequence = 0;
                rules.forEach(rule => {
                    const link = document.createElement('a');
                    link.classList.add('mjk-link-accent');
                    link.href = `${protocol}//${host}/#/graph/${rule.id}`;
                    link.target = '_self';
                    link.textContent = ++sequence + "、" + rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]";
                    link.onclick = () => {
                        window.location.hash = '#/';
                        selectCardIds = rule.cardIds;
                    };
                    bindLinkClickHighlight(link);
                    ruleCell.appendChild(link);
                    ruleCell.appendChild(document.createElement('br'));
                });
                row.appendChild(roomCell);
                row.appendChild(deviceCell);
                row.appendChild(ruleCell);
                devTbody.appendChild(row);
            });
            devTable.appendChild(devTbody);

            //变量表格
            const varTable = document.createElement('table');
            varTable.id = 'varTable';
            varTable.className = 'mjk-table';
            varTable.border = '1';
            varTable.cellSpacing = '0';
            varTable.cellPadding = '5';
            varTable.style.width = '100%';
            varTable.style.border = '1px soild black';
            varTable.style.userSelect = 'text';
            varTable.style.borderCollapse = 'collapse';
            varTable.style.display = 'none';

            const varThead = document.createElement('thead');
            const varHeaderRow = document.createElement('tr');
            const varScopeHeader = document.createElement('th');
            const varTypeHeader = document.createElement('th');
            const varNameHeader = document.createElement('th');
            const varValueHeader = document.createElement('th');
            const varRuleHeader = document.createElement('th');
            const varTbody = document.createElement('tbody');

            let varScopeSortOrder = 'asc';
            let varTypeSortOrder = 'asc';
            let varNameSortOrder = 'asc';
            let varValueSortOrder = 'asc';
            let varRuleSortOrder = 'asc';
            const varUpdateSortMarkers = () => {
                varScopeHeader.innerHTML = `变量范围 ${varScopeSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                varTypeHeader.innerHTML = `变量类型 ${varTypeSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                varNameHeader.innerHTML = `变量名 ${varNameSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                varValueHeader.innerHTML = `变量值 ${varValueSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                varRuleHeader.innerHTML = `自动化名称 ${varRuleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
            };

            varScopeHeader.textContent = '变量范围';
            varScopeHeader.style.textWrap = 'nowrap';
            varTypeHeader.textContent = '变量类型';
            varTypeHeader.style.textWrap = 'nowrap';
            varNameHeader.textContent = '变量名';
            varNameHeader.style.textWrap = 'nowrap';
            varValueHeader.textContent = '变量值';
            varValueHeader.style.textWrap = 'nowrap';
            varRuleHeader.textContent = '自动化名称';
            varRuleHeader.style.textWrap = 'nowrap';

            varScopeHeader.onclick = () => {
                varScopeSortOrder = varScopeSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(varTbody, 0, varScopeSortOrder);
                varUpdateSortMarkers();
            };
            varTypeHeader.onclick = () => {
                varTypeSortOrder = varTypeSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(varTbody, 1, varTypeSortOrder);
                varUpdateSortMarkers();
            };
            varNameHeader.onclick = () => {
                varNameSortOrder = varNameSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(varTbody, 2, varNameSortOrder);
                varUpdateSortMarkers();
            };
            varValueHeader.onclick = () => {
                varValueSortOrder = varValueSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(varTbody, 3, varValueSortOrder);
                varUpdateSortMarkers();
            };
            varRuleHeader.onclick = () => {
                varRuleSortOrder = varRuleSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(varTbody, 4, varRuleSortOrder);
                varUpdateSortMarkers();
            };

            varHeaderRow.appendChild(varScopeHeader);
            varHeaderRow.appendChild(varTypeHeader);
            varHeaderRow.appendChild(varNameHeader);
            varHeaderRow.appendChild(varValueHeader);
            varHeaderRow.appendChild(varRuleHeader);
            varThead.appendChild(varHeaderRow);
            varTable.appendChild(varThead);
            Object.entries(varRuleData).forEach(([vid, data]) => {
                const rules = data.rules;
                const varData = varMap[vid];
                if (varData != null) {
                    const row = document.createElement('tr');

                    const varScopeCell = document.createElement('td');
                    varScopeCell.textContent = varData.scope;
                    varScopeCell.style.textWrap = 'nowrap';
                    const varTypeCell = document.createElement('td');
                    varTypeCell.textContent = varData.type;
                    varTypeCell.style.textWrap = 'nowrap';
                    const varNameCell = document.createElement('td');
                    varNameCell.textContent = varData.name;
                    varNameCell.style.textWrap = 'nowrap';
                    const varValueCell = document.createElement('td');
                    varValueCell.textContent = varData.value;
                    // 明确允许换行,优先于通用表格 nowrap
                    varValueCell.style.whiteSpace = 'normal';

                    const varRuleCell = document.createElement('td');
                    const protocol = window.location.protocol;
                    const host = window.location.host;
                    let sequence = 0;
                    rules.forEach(rule => {
                        const link = document.createElement('a');
                        link.classList.add('mjk-link-accent');
                        link.href = `${protocol}//${host}/#/graph/${rule.id}`;
                        link.target = '_self';
                        link.textContent = ++sequence + "、" + rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]";
                        link.onclick = () => {
                            window.location.hash = '#/';
                            selectCardIds = rule.cardIds;
                        };
                        bindLinkClickHighlight(link);
                        varRuleCell.appendChild(link);
                        varRuleCell.appendChild(document.createElement('br'));
                    });
                    row.appendChild(varScopeCell);
                    row.appendChild(varTypeCell);
                    row.appendChild(varNameCell);
                    row.appendChild(varValueCell);
                    row.appendChild(varRuleCell);
                    varTbody.appendChild(row);
                }
            });
            varTable.appendChild(varTbody);

            //虚拟事件表格
            const eventTable = document.createElement('table');
            eventTable.id = 'eventTable';
            eventTable.className = 'mjk-table';
            eventTable.border = '1';
            eventTable.cellSpacing = '0';
            eventTable.cellPadding = '5';
            eventTable.style.width = '100%';
            eventTable.style.border = '1px soild black';
            eventTable.style.userSelect = 'text';
            eventTable.style.borderCollapse = 'collapse';
            eventTable.style.display = 'none';

            const eventThead = document.createElement('thead');
            const eventHeaderRow = document.createElement('tr');
            const eventTypeHeader = document.createElement('th');
            const eventNameHeader = document.createElement('th');
            const eventRuleHeader = document.createElement('th');
            const eventTbody = document.createElement('tbody');

            let eventTypeSortOrder = 'asc';
            let eventNameSortOrder = 'asc';
            let eventRuleSortOrder = 'asc';
            const eventUpdateSortMarkers = () => {
                eventTypeHeader.innerHTML = `虚拟事件类型 ${eventTypeSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                eventNameHeader.innerHTML = `事件名称 ${eventNameSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                eventRuleHeader.innerHTML = `自动化名称 ${eventRuleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
            };

            eventTypeHeader.textContent = '虚拟事件类型';
            eventTypeHeader.style.textWrap = 'nowrap';
            eventNameHeader.textContent = '事件名称';
            eventNameHeader.style.textWrap = 'nowrap';
            eventRuleHeader.textContent = '自动化名称';
            eventRuleHeader.style.textWrap = 'nowrap';

            eventTypeHeader.onclick = () => {
                eventTypeSortOrder = eventTypeSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(eventTbody, 0, eventTypeSortOrder);
                eventUpdateSortMarkers();
            };
            eventNameHeader.onclick = () => {
                eventNameSortOrder = eventNameSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(eventTbody, 1, eventNameSortOrder);
                eventUpdateSortMarkers();
            };
            eventRuleHeader.onclick = () => {
                eventRuleSortOrder = eventRuleSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(eventTbody, 2, eventRuleSortOrder);
                eventUpdateSortMarkers();
            };

            eventHeaderRow.appendChild(eventTypeHeader);
            eventHeaderRow.appendChild(eventNameHeader);
            eventHeaderRow.appendChild(eventRuleHeader);
            eventThead.appendChild(eventHeaderRow);
            eventTable.appendChild(eventThead);
            Object.entries(sendEventRuleData).forEach(([eventName, data]) => {
                const rules = data.rules;
                const row = document.createElement('tr');

                const eventTypeCell = document.createElement('td');
                eventTypeCell.textContent = "发送";
                eventTypeCell.style.textWrap = 'nowrap';
                const eventNameCell = document.createElement('td');
                eventNameCell.textContent = eventName;
                eventNameCell.style.textWrap = 'nowrap';
                const eventRuleCell = document.createElement('td');
                const protocol = window.location.protocol;
                const host = window.location.host;
                let sequence = 0;
                rules.forEach(rule => {
                    const link = document.createElement('a');
                    link.classList.add('mjk-link-accent');
                    link.href = `${protocol}//${host}/#/graph/${rule.id}`;
                    link.target = '_self';
                    link.textContent = ++sequence + "、" + rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]";
                    link.onclick = () => {
                        window.location.hash = '#/';
                        selectCardIds = rule.cardIds;
                    };
                    bindLinkClickHighlight(link);
                    eventRuleCell.appendChild(link);
                    eventRuleCell.appendChild(document.createElement('br'));
                });
                row.appendChild(eventTypeCell);
                row.appendChild(eventNameCell);
                row.appendChild(eventRuleCell);
                eventTbody.appendChild(row);
            });
            Object.entries(receiveEventRuleData).forEach(([eventName, data]) => {
                const rules = data.rules;
                const row = document.createElement('tr');

                const eventTypeCell = document.createElement('td');
                eventTypeCell.textContent = "接收";
                eventTypeCell.style.textWrap = 'nowrap';
                const eventNameCell = document.createElement('td');
                eventNameCell.textContent = eventName;
                eventNameCell.style.textWrap = 'nowrap';
                const eventRuleCell = document.createElement('td');
                const protocol = window.location.protocol;
                const host = window.location.host;
                let sequence = 0;
                rules.forEach(rule => {
                    const link = document.createElement('a');
                    link.classList.add('mjk-link-accent');
                    link.href = `${protocol}//${host}/#/graph/${rule.id}`;
                    link.target = '_self';
                    link.textContent = ++sequence + "、" + rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]";
                    link.onclick = () => {
                        window.location.hash = '#/';
                        selectCardIds = rule.cardIds;
                    };
                    bindLinkClickHighlight(link);
                    eventRuleCell.appendChild(link);
                    eventRuleCell.appendChild(document.createElement('br'));
                });
                row.appendChild(eventTypeCell);
                row.appendChild(eventNameCell);
                row.appendChild(eventRuleCell);
                eventTbody.appendChild(row);
            });


            eventTable.appendChild(eventTbody);

            // 顶栏固定到 body,避免 Safari 在父容器折叠时不渲染固定子元素
            container.appendChild(devTable);
            container.appendChild(varTable);
            container.appendChild(eventTable);
            document.body.appendChild(container);
            // 先移除可能存在的旧顶栏,再追加新的顶栏
            const existedTopBar = document.getElementById('topBar');
            if (existedTopBar && existedTopBar !== topBar && existedTopBar.parentElement) {
                existedTopBar.parentElement.removeChild(existedTopBar);
            }
            document.body.appendChild(topBar);

            devUpdateSortMarkers();
            varUpdateSortMarkers();
            eventUpdateSortMarkers();

            function filterDevTable(roomName, deviceKeyword, ruleKeyword) {
                const rows = Array.from(devTable.rows);
                rows.forEach(row => {
                    const roomText = row.cells[0].textContent;
                    const deviceText = row.cells[1].textContent.toLowerCase();
                    const ruleText = row.cells[2].textContent.toLowerCase();
                    if ((roomName === '' || roomText === roomName) && deviceText.includes(deviceKeyword.toLowerCase()) && ruleText.includes(ruleKeyword.toLowerCase())) {
                        row.style.display = '';
                    } else {
                        row.style.display = 'none';
                    }
                });
            }

            function filterVarTable(varScope, varType, varName, varValue, ruleName) {
                const rows = Array.from(varTable.rows);
                rows.forEach(row => {
                    const varScopeText = row.cells[0].textContent;
                    const varTypeText = row.cells[1].textContent;
                    const varNameText = row.cells[2].textContent.toLowerCase();
                    const varValueText = row.cells[3].textContent.toLowerCase();
                    const ruleNameText = row.cells[4].textContent.toLowerCase();
                    if ((varScope === '' || varScope === varScopeText)
                        && (varType === '' || varType === varTypeText)
                        && varNameText.includes(varName.toLowerCase())
                        && varValueText.includes(varValue.toLowerCase())
                        && ruleNameText.includes(ruleName.toLowerCase())) {
                        row.style.display = '';
                    } else {
                        row.style.display = 'none';
                    }
                });
            }

            function filterEventTable(eventType, eventName, ruleName) {
                const rows = Array.from(eventTable.rows);
                rows.forEach(row => {
                    const eventTypeText = row.cells[0].textContent;
                    const eventNameText = row.cells[1].textContent.toLowerCase();
                    const ruleNameText = row.cells[2].textContent.toLowerCase();
                    if ((eventType === '' || eventType === eventTypeText)
                        && eventNameText.includes(eventName.toLowerCase())
                        && ruleNameText.includes(ruleName.toLowerCase())) {
                        row.style.display = '';
                    } else {
                        row.style.display = 'none';
                    }
                });
            }

            autoCollapse();
            handleUrlChange();
        } catch (error) {
            isInit = false;
            console.error('调用 API 时出错:', error);
        }
    };

    const selectDevices = async () => {
        // await sleep(600);
        if (selectCardIds !== '') {
            if (editor && editor.transformTool) {
                editor.transformTool.fitToBestPos();
                await sleep(600);
            }
            const cardIds = selectCardIds.split(',');
            for (const cardId of cardIds) {
                if (cardId.trim() !== '') {

                    let targetElement = document.querySelector("[id='" + cardId.trim() + "'] > div > div");
                    if (targetElement) {
                        targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
                    }
                }
            }
        }

        selectCardIds = '';
        closeGraphContainer();

        const graphDevTableDiv = document.createElement('div');
        const graphDevSpan = document.createElement('span');
        graphDevSpan.textContent = "当前自动化涉及的设备列表:";
        graphDevSpan.style.userSelect = 'text';
        graphDevTableDiv.id = 'graphDevTableDiv';
        graphDevTableDiv.appendChild(graphDevSpan);
        const graphDevTable = document.createElement('table');
        graphDevTable.border = '1';
        graphDevTable.cellSpacing = '0';
        graphDevTable.cellPadding = '5';
        graphDevTable.style.width = '100%';
        graphDevTable.style.border = '1px soild black';
        graphDevTable.style.borderCollapse = 'collapse';
        graphDevTable.style.userSelect = 'text';
        graphDevTableDiv.appendChild(graphDevTable);

        const graphDevThead = document.createElement('thead');
        const graphDevHeaderRow = document.createElement('tr');
        const graphDevRoomHeader = document.createElement('th');
        const graphDevInfoHeader = document.createElement('th');
        const graphDevTbody = document.createElement('tbody');

        graphDevRoomHeader.textContent = '房间';
        graphDevRoomHeader.style.textWrap = 'nowrap';
        graphDevInfoHeader.textContent = '设备名称';
        graphDevInfoHeader.style.textWrap = 'nowrap';

        let ruleId = window.location.hash.split('/')[2];
        const ruleContent = await callAPI('getGraph', {id: ruleId});

        const dids = new Set(ruleContent.nodes.map(n => n.props?.did).filter(did => did !== undefined));
        dids.forEach(did => {
            const row = document.createElement('tr');
            const roomCell = document.createElement('td');
            roomCell.textContent = devMap[did]?.roomName ?? `未知`;
            roomCell.style.textWrap = 'nowrap';
            const deviceCell = document.createElement('td');
            deviceCell.style.textWrap = 'nowrap';
            const cards = ruleContent.nodes
                .filter(node => node.props && node.id && node.props.did === did)
                .map(node => node.id);
            const link = document.createElement('a');
            link.classList.add('mjk-link-accent');
            link.href = `javascript:void(0);`;
            link.textContent = devMap[did]?.name + " [" + cards.length + "]" ?? `did: ${did}`;
            link.onclick = () => {
                const cssCards = document.querySelectorAll('.card,.simple-card');
                if (cssCards) {
                    cssCards.forEach(card => {
                        card.style.backgroundColor = '';
                    });
                }
                if (cards) {
                    if (editor && editor.transformTool) {
                        editor.transformTool.fitToBestPos();
                        sleep(600);
                    }
                    cards.forEach(cardId => {
                        let targetElement = document.querySelector("[id='" + cardId.trim() + "'] > div > div");
                        if (targetElement) {
                            targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
                        }
                    });
                }
            };
            bindLinkClickHighlight(link);
            deviceCell.appendChild(link);
            row.appendChild(roomCell);
            row.appendChild(deviceCell);
            graphDevTbody.appendChild(row);
        });

        const graphVarTableDiv = document.createElement('div');
        graphVarTableDiv.textContent = "当前自动化涉及的变量列表:";
        graphVarTableDiv.style.marginTop = '10px';
        graphVarTableDiv.id = 'graphVarTableDiv';
        graphVarTableDiv.style.userSelect = 'text';
        const graphVarTable = document.createElement('table');
        graphVarTable.border = '1';
        graphVarTable.cellSpacing = '0';
        graphVarTable.cellPadding = '5';
        graphVarTable.style.width = '100%';
        graphVarTable.style.border = '1px soild black';
        graphVarTable.style.borderCollapse = 'collapse';
        graphVarTable.style.userSelect = 'text';
        graphVarTableDiv.appendChild(graphVarTable);

        const graphVarThead = document.createElement('thead');
        const graphVarHeaderRow = document.createElement('tr');
        const graphVarScopeHeader = document.createElement('th');
        const graphVarTypeHeader = document.createElement('th');
        const graphVarNameHeader = document.createElement('th');
        const graphVarValueHeader = document.createElement('th');
        const graphVarTbody = document.createElement('tbody');

        graphVarScopeHeader.textContent = '范围';
        graphVarScopeHeader.style.textWrap = 'nowrap';
        graphVarTypeHeader.textContent = '类型';
        graphVarTypeHeader.style.textWrap = 'nowrap';
        graphVarNameHeader.textContent = '变量名称';
        graphVarNameHeader.style.textWrap = 'nowrap';
        graphVarValueHeader.textContent = '变量值';

        let varMap = {};
        const varScopes = (await callAPI('getVarScopeList', {})).scopes;
        for (const scope of varScopes) {
            if (scope === "global" || scope === "R" + ruleId) {
                const vars = await callAPI('getVarList', {scope: scope});
                Object.entries(vars).forEach(([vid, v]) => {
                    varMap[vid] = {
                        name: v.userData.name,
                        scope: (scope === "global" ? "全局" : "局部"),
                        type: (v.type === "string" ? "文本" : "数值"),
                        value: v.value
                    }
                });
            }
        }

        const varids = new Set(ruleContent.nodes
            .flatMap(node => {
                if (!node.props) return [];

                const ids = [];
                // 直接变量卡片的ID
                if (node.props.scope) {
                    ids.push(node.props.id);
                }
                // 参数中的变量ID
                if (node.props.arguments?.length > 0) {
                    ids.push(...node.props.arguments
                        .filter(arg => arg.id)
                        .map(arg => arg.id)
                    );
                }
                return ids;
            })
        );
        varids.forEach(vid => {
            const row = document.createElement('tr');
            const varScopeCell = document.createElement('td');
            varScopeCell.textContent = varMap[vid]?.scope ?? `未知`;
            varScopeCell.style.textWrap = 'nowrap';
            const varTypeCell = document.createElement('td');
            varTypeCell.textContent = varMap[vid]?.type ?? `未知`;
            varTypeCell.style.textWrap = 'nowrap';
            const varNameCell = document.createElement('td');
            varNameCell.style.textWrap = 'nowrap';
            const cards = ruleContent.nodes
                .filter(node => node.props && node.id && ((node.props.scope && node.props.id === vid) || (node.props.arguments && node.props.arguments.some(arg => arg.id === vid))))
                .map(node => node.id);

            const link = document.createElement('a');
            link.classList.add('mjk-link-accent');
            link.href = `javascript:void(0);`;
            link.textContent = varMap[vid]?.name + " [" + cards.length + "]" ?? `未知`;
            link.onclick = () => {
                const cssCards = document.querySelectorAll('.card,.simple-card');
                if (cssCards) {
                    cssCards.forEach(card => {
                        card.style.backgroundColor = '';
                    });
                }
                if (cards) {
                    if (editor && editor.transformTool) {
                        editor.transformTool.fitToBestPos();
                        sleep(600);
                    }
                    cards.forEach(cardId => {
                        let targetElement = document.querySelector("[id='" + cardId.trim() + "'] > div > div");
                        if (targetElement) {
                            targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
                        }
                    });
                }
            };
            bindLinkClickHighlight(link);
            varNameCell.appendChild(link);
            const varValueCell = document.createElement('td');
            varValueCell.textContent = varMap[vid]?.value ?? `未知`;
            row.appendChild(varScopeCell);
            row.appendChild(varTypeCell);
            row.appendChild(varNameCell);
            row.appendChild(varValueCell);
            graphVarTbody.appendChild(row);
        });

        const graphEventTableDiv = document.createElement('div');
        graphEventTableDiv.textContent = "当前自动化涉及的虚拟事件列表:";
        graphEventTableDiv.style.marginTop = '10px';
        graphEventTableDiv.id = 'graphEventTableDiv';
        graphEventTableDiv.style.userSelect = 'text';
        const graphEventTable = document.createElement('table');
        graphEventTable.border = '1';
        graphEventTable.cellSpacing = '0';
        graphEventTable.cellPadding = '5';
        graphEventTable.style.width = '100%';
        graphEventTable.style.border = '1px soild black';
        graphEventTable.style.borderCollapse = 'collapse';
        graphEventTable.style.userSelect = 'text';
        graphEventTableDiv.appendChild(graphEventTable);

        const graphEventThead = document.createElement('thead');
        const graphEventHeaderRow = document.createElement('tr');
        const graphEventTypeHeader = document.createElement('th');
        const graphEventNameHeader = document.createElement('th');
        const graphEventTbody = document.createElement('tbody');

        graphEventTypeHeader.textContent = '虚拟事件类型';
        graphEventTypeHeader.style.textWrap = 'nowrap';
        graphEventNameHeader.textContent = '虚拟事件名称';
        graphEventNameHeader.style.textWrap = 'nowrap';

        const sendEventCards = new Set(ruleContent.nodes.map(n => {
            return (n.props && n.cfg && n.id && n.type === "deviceOutput"
                && n.cfg.urn && n.cfg.urn.indexOf("device:gateway") > -1
                && n.props.siid && n.props.siid === 4 && n.props.aiid && n.props.aiid === 1
                && n.props.ins && n.props.ins[0] && n.props.ins[0].piid && n.props.ins[0].piid === 1
            ) ? {
                cid: n.id,
                eventName: n.props.ins[0].value ? n.props.ins[0].value : n.props.ins[0].id + "$$" + n.props.ins[0].scope
            } : undefined;
        }).filter(card => card !== undefined));
        const sendGrouped = Array.from(sendEventCards).reduce((acc, {eventName, cid}) => {
            if (!acc[eventName]) {
                acc[eventName] = [];
            }
            acc[eventName].push(cid);
            return acc;
        }, {});
        const groupedSendEventCards = Object.entries(sendGrouped).map(([eventName, cids]) => ({
            eventName,
            cids
        }));
        const receiveEventCards = new Set(ruleContent.nodes.map(n => {
            return (n.props && n.cfg && n.id && n.type === "deviceInput"
                && n.cfg.urn && n.cfg.urn.indexOf("device:gateway") > -1
                && n.props.siid && n.props.siid === 4 && n.props.eiid && n.props.eiid === 1
                && n.props.arguments && n.props.arguments[0] && n.props.arguments[0].piid && n.props.arguments[0].piid === 1
            ) ? {
                cid: n.id, eventName: n.props.arguments[0].v1
            } : undefined;
        }).filter(card => card !== undefined));
        const receiveGrouped = Array.from(receiveEventCards).reduce((acc, {eventName, cid}) => {
            if (!acc[eventName]) {
                acc[eventName] = [];
            }
            acc[eventName].push(cid);
            return acc;
        }, {});
        const groupedReceiveEventCards = Object.entries(receiveGrouped).map(([eventName, cids]) => ({
            eventName,
            cids
        }));
        const createEventRow = (event, eventType) => {
            const row = document.createElement('tr');
            const eventTypeCell = document.createElement('td');
            eventTypeCell.textContent = eventType;
            eventTypeCell.style.textWrap = 'nowrap';
            const eventNameCell = document.createElement('td');
            eventNameCell.style.textWrap = 'nowrap';
            const cards = event.cids;
            const link = document.createElement('a');
            link.classList.add('mjk-link-accent');
            link.href = `javascript:void(0);`;
            let eventName = event.eventName;
            if (eventName.indexOf("$$") > -1) {
                let vid = event.eventName.split("$$")[0];
                let scope = event.eventName.split("$$")[1];
                const varData = varMap[vid];
                if (varData) {
                    eventName = "变量名:${" + varData.name + "},当前值:" + varData.value;
                }
            }
            link.textContent = eventName + " [" + cards.length + "]";
            link.onclick = () => {
                const cssCards = document.querySelectorAll('.card,.simple-card');
                if (cssCards) {
                    cssCards.forEach(card => {
                        card.style.backgroundColor = '';
                    });
                }
                if (cards) {
                    if (editor && editor.transformTool) {
                        editor.transformTool.fitToBestPos();
                        sleep(600);
                    }
                    cards.forEach(cardId => {
                        let targetElement = document.querySelector("[id='" + cardId.trim() + "'] > div > div");
                        if (targetElement) {
                            targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
                        }
                    });
                }
            };
            bindLinkClickHighlight(link);
            eventNameCell.appendChild(link);
            row.appendChild(eventTypeCell);
            row.appendChild(eventNameCell);
            graphEventTbody.appendChild(row);
        };

        groupedSendEventCards.forEach(event => createEventRow(event, '发送'));
        groupedReceiveEventCards.forEach(event => createEventRow(event, '接收'));

        const graphRemarkDiv = document.createElement('div');
        graphRemarkDiv.textContent = "提示:设备名称或变量名称后的括号内显示的数字为当前自动化涉及的卡片数量,点击设备名称或变量名称可高亮显示对应卡片!";
        graphRemarkDiv.style.color = 'red';
        graphRemarkDiv.style.marginTop = '10px';

        graphDevHeaderRow.appendChild(graphDevRoomHeader);
        graphDevHeaderRow.appendChild(graphDevInfoHeader);
        graphDevThead.appendChild(graphDevHeaderRow);
        graphDevTable.appendChild(graphDevThead);
        graphDevTable.appendChild(graphDevTbody);
        sortTable(graphDevTbody, 0, 'asc');

        const graphContainer = document.getElementById('graph-list-container') || document.createElement('div');
        graphContainer.id = 'graph-list-container';
        graphContainer.style.position = 'fixed';
        graphContainer.style.top = '39px';
        graphContainer.style.right = '40px';
        // 宽度由 CSS 变量 --mjk-graph-width 控制(默认 680px)
        // 高度由 CSS 控制(calc(var(--mjk-height) - 50px))
        graphContainer.style.overflowY = 'scroll';
        graphContainer.style.backgroundColor = 'white';
        graphContainer.style.border = '1px solid #ccc';
        graphContainer.style.paddingTop = '0px';
        graphContainer.style.zIndex = 2147483645; // 紧随主容器与顶栏之下
        graphContainer.style.transform = 'translateZ(0)';
        graphContainer.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
        graphContainer.style.display = 'none';
        graphContainer.style.overflow = 'auto';
        // 高度由 CSS calc(var(--mjk-height) - 50px) 控制

        if (!document.getElementById('graphDevTableDiv')) {
            graphContainer.appendChild(graphDevTableDiv);
        }

        graphVarHeaderRow.appendChild(graphVarScopeHeader);
        graphVarHeaderRow.appendChild(graphVarTypeHeader);
        graphVarHeaderRow.appendChild(graphVarNameHeader);
        graphVarHeaderRow.appendChild(graphVarValueHeader);
        graphVarThead.appendChild(graphVarHeaderRow);
        graphVarTable.appendChild(graphVarThead);
        graphVarTable.appendChild(graphVarTbody);
        sortTable(graphVarTbody, 0, 'asc');
        if (varids.size > 0) {
            if (!document.getElementById('graphVarTableDiv')) {
                graphContainer.appendChild(graphVarTableDiv);
            }
        }

        graphEventHeaderRow.appendChild(graphEventTypeHeader);
        graphEventHeaderRow.appendChild(graphEventNameHeader);
        graphEventThead.appendChild(graphEventHeaderRow);
        graphEventTable.appendChild(graphEventThead);
        graphEventTable.appendChild(graphEventTbody);
        sortTable(graphEventTbody, 0, 'asc');
        if (groupedSendEventCards.length > 0 || groupedReceiveEventCards.length > 0) {
            if (!document.getElementById('graphEventTableDiv')) {
                graphContainer.appendChild(graphEventTableDiv);
            }
        }

        //graphContainer.appendChild(graphRemarkDiv);
        if (!document.getElementById('graph-list-container')) {
            document.body.appendChild(graphContainer);
        }
    };

    function sortTable(tbody, columnIndex, sortOrder) {
        const rows = Array.from(tbody.rows);
        const sortedRows = rows.sort((a, b) => {
            const aText = a.cells[columnIndex].textContent;
            const bText = b.cells[columnIndex].textContent;
            if (sortOrder === 'asc') {
                return aText.localeCompare(bText);
            } else {
                return bText.localeCompare(aText);
            }
        });
        tbody.innerHTML = '';
        sortedRows.forEach(row => tbody.appendChild(row));
    }

    //设备列表视图
    function handleChangeViewBtnClick(type) {
        const varTable = document.getElementById('varTable');
        const devTable = document.getElementById('devTable');
        const eventTable = document.getElementById('eventTable');
        const devFilterContainer = document.getElementById('devFilterContainer');
        const varFilterContainer = document.getElementById('varFilterContainer');
        const eventFilterContainer = document.getElementById('eventFilterContainer');

        if (type === 'dev') {
            devTable.style.display = '';
            varTable.style.display = 'none';
            eventTable.style.display = 'none';
            devFilterContainer.style.display = '';
            varFilterContainer.style.display = 'none';
            eventFilterContainer.style.display = 'none';
        } else if (type === 'var') {
            devTable.style.display = 'none';
            varTable.style.display = '';
            eventTable.style.display = 'none';
            devFilterContainer.style.display = 'none';
            varFilterContainer.style.display = '';
            eventFilterContainer.style.display = 'none';
        } else if (type === 'event') {
            devTable.style.display = 'none';
            varTable.style.display = 'none';
            eventTable.style.display = '';
            devFilterContainer.style.display = 'none';
            varFilterContainer.style.display = 'none';
            eventFilterContainer.style.display = '';
        }
    }


    function handleOptionsBtnClick() {
        const optionsContainer = document.getElementById('optionsContainer');
        if (optionsContainer) {
            if (optionsContainer.style.display === 'none') {
                optionsContainer.style.display = '';
            } else {
                optionsContainer.style.display = 'none';
            }
        }
    }

    function handleCollapseBtnClick() {
        const container = document.getElementById('device-rule-map');
        if (container) {
            if (isCollapsed) {
                expand();
            } else {
                collapse();
            }
        }
    }

    function collapse() {
        const container = document.getElementById('device-rule-map');
        if (container) {
            const collapseButton = document.getElementById('collapseButton');
            const topBar = document.getElementById('topBar');
            topBar.style.width = '';
            document.documentElement.style.setProperty('--mjk-topbar-width', (parseInt(graphWidth, 10) || 0) + 'px');
            container.style.height = '0px';
            container.style.width = '0px';
            container.style.top = '0';
            container.style.right = '-1px';
            collapseButton.textContent = '展开';
            isCollapsed = true;
        }
    }

    function expand() {
        const container = document.getElementById('device-rule-map');
        if (container) {
            const collapseButton = document.getElementById('collapseButton');
            const topBar = document.getElementById('topBar');
            topBar.style.width = '';
            document.documentElement.style.setProperty('--mjk-topbar-width', (parseInt(windowWidth, 10) || 0) + 'px');
            container.style.width = '';
            container.style.height = '';
            container.style.top = '10px';
            container.style.right = '40px';
            collapseButton.textContent = '折叠';
            isCollapsed = false;
        }
    }

    function autoFitContent() {
        if (enableAutoFitContent && editor && editor.transformTool) {
            editor.transformTool.fitToBestPos();
        }
    }

    function autoCollapse() {
        if (enableAutoCollapseCheck) {
            collapse();
        }
    }

    function enhancedDisplayLog() {
        //监听画布变化
        const canvas = document.getElementById('canvas-root');
        if (canvas) {
            const config = {attributes: false, childList: true, subtree: true};
            const callback = function (mutationsList, observer) {
                if (enableEnhancedDisplayLog) {
                    let element = document.querySelector('.panel-log-card-blink');
                    if (element && element.style.outline !== "red solid 20px") {
                        element.style.outline = "red solid 10px";
                    }
                    let animateElement = document.querySelector('animate');
                    if (animateElement && animateElement.getAttribute('stroke-width') != '10') {
                        let pathElement = animateElement.parentElement;
                        pathElement.setAttribute('stroke-width', '10');
                        if (pathElement) {
                            let gElement = pathElement.parentElement;
                            gElement.setAttribute('stroke', 'red');
                        }
                    }
                }
            };
            const observer = new MutationObserver(callback);
            observer.observe(canvas, config);
        }
    }

    function addShortcutHelp() {
        const addedSchortcut = document.getElementById('addedSchortcut');
        if (addedSchortcut) {
            return;
        }
        const helpLeft = document.querySelector('.help-modal-left');
        if (helpLeft) {
            const leftContent = `
          <div></div>
          <div class="help-modal-item-label" id="addedSchortcut">自适应画布</div>
          <div class="help-modal-item-btn">Ctrl 或 ⌘</div>
          <div class="help-modal-item-btn" style="width: 41px;">B</div>
          <div class="help-modal-item-label">关闭画布</div>
          <div class="help-modal-item-btn">Ctrl</div>
          <div class="help-modal-item-btn" style="width: 41px;">W</div>
          <div class="help-modal-item-label">关闭助手</div>
          <div class="help-modal-item-btn">Ctrl</div>
          <div class="help-modal-item-btn" style="width: 41px;">Q</div>
          `;
            helpLeft.insertAdjacentHTML('beforeend', leftContent);
        }
        const helpRight = document.querySelector('.help-modal-right');
        if (helpRight) {
            const rightContent = `
          <div></div>
          <div class="help-modal-item-label">折叠助手</div>
          <div class="help-modal-item-btn" style="width: auto;">Ctrl 或 ⌘</div>
          <div class="help-modal-item-btn" style="width: 41px;">E</div>
          <div class="help-modal-item-label">刷新助手</div>
          <div class="help-modal-item-btn">Ctrl</div>
          <div class="help-modal-item-btn" style="width: 41px;">R</div>
          `;
            helpRight.insertAdjacentHTML('beforeend', rightContent);
        }

        GM_addStyle('.help-modal .help-modal-middle {grid-template-rows: 32px 32px 32px 32px 32px;}');
        GM_addStyle('.help-modal .help-modal-right {grid-template-rows: 32px 32px 32px 32px 32px;}');
        GM_addStyle('.help-modal {height: 238px;}');
    }

    function changeRuleListStyle(count) {
        GM_addStyle('.automation-rule-page .ant-spin-container .content-scroll-wrapper .rule-list {    grid-template-columns: repeat(' + count + ', 1fr);}')
    }

    function sortVars() {
        const varLists = document.querySelectorAll(".var-list");
        varLists.forEach(varList => {
            const commonVarCells = Array.from(varList.querySelectorAll(".common-var-cell"));

            const cellsWithTitle = commonVarCells.map(cell => ({
                cell: cell,
                title: (cell.querySelector(".var-name")?.getAttribute("title") || "").toUpperCase()
            }));

            cellsWithTitle.sort((a, b) => a.title.localeCompare(b.title));

            const fragment = document.createDocumentFragment();
            cellsWithTitle.forEach(item => fragment.appendChild(item.cell));

            varList.innerHTML = '';
            varList.appendChild(fragment);
        });
    }

    let sleep = function (ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }

    function isMiJiaJiKePage() {
        return document.title === "米家自动化极客版" && !document.querySelector('.pin-code-with-keyboard') && editor;
    }

    function closeGraphContainer() {
        const graphContainer = document.getElementById('graph-list-container');
        const graphListDetailButton = document.getElementById('graphListDetailButton');
        if (graphContainer) {
            document.body.removeChild(graphContainer);
        }
        if (graphListDetailButton) {
            if (window.location.hash.match(/^#\/graph\/.*/g)) {
                graphListDetailButton.style.display = '';
            } else {
                graphListDetailButton.style.display = 'none';
            }
        }
    }

    function handleGraphListDetailBtnClick() {
        const graphContainer = document.getElementById('graph-list-container');
        if (graphContainer) {
            collapse();
            if (graphContainer.style.display === 'none') {
                graphContainer.style.display = '';
            } else {
                graphContainer.style.display = 'none';
            }
            document.documentElement.style.setProperty('--mjk-topbar-width', (parseInt(graphWidth, 10) || 0) + 'px');
        }
    }

    function handleUrlChange() {
        if (isMiJiaJiKePage()) {
            executeScript();
            closeGraphContainer();
            if (window.location.hash.match(/^#\/graph\/.*/g)) {
                setTimeout(function () {
                    autoFitContent();
                    enhancedDisplayLog();
                    autoCollapse();
                    addShortcutHelp();
                    selectDevices();
                }, 500);
            }
            if (window.location.hash.match(/^#\/vars/g)) {
                setTimeout(function () {
                    sortVars();
                }, 500);
                sortVars();
            }
        }
    }

    //页面变化
    window.addEventListener('popstate', handleUrlChange);
    window.addEventListener('hashchange', handleUrlChange);
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;
    history.pushState = function () {
        originalPushState.apply(this, arguments);
        handleUrlChange();
    };
    history.replaceState = function () {
        originalReplaceState.apply(this, arguments);
        handleUrlChange();
    };
    //快捷键
    document.addEventListener('keydown', function (event) {
        if ((event.metaKey || event.ctrlKey) && event.key === 'e') {
            event.preventDefault();
            if (document.getElementById('collapseButton')) {
                document.getElementById('collapseButton').click();
            }
        }
        if (event.ctrlKey && event.key === 'q') {
            event.preventDefault();
            if (document.getElementById('closeButton')) {
                document.getElementById('closeButton').click();
            }
        }
        if (event.ctrlKey && event.key === 'r') {
            event.preventDefault();
            if (document.getElementById('refreshButton')) {
                document.getElementById('refreshButton').click();
            }
        }
        if ((event.metaKey || event.ctrlKey) && event.key === 'b') {
            event.preventDefault();
            if (editor && editor.transformTool) {
                editor.transformTool.fitToBestPos();
            }
        }
        if (event.ctrlKey && event.key === 'w') {
            event.preventDefault();
            const selectedMenuItem = document.querySelector('.app-header-menu-item-selected');
            if (selectedMenuItem) {
                const actionElement = selectedMenuItem.querySelector('.app-header-menu-item-action');
                if (actionElement) {
                    actionElement.click();
                }
            }
        }

    });
    window.onload = function () {
        //监控登录页面
        const loginForm = document.querySelector('.account-content');
        if (loginForm) {
            const config = {attributes: true, childList: true, subtree: true};
            const callback = function (mutationsList, observer) {
                setTimeout(function () {
                    handleUrlChange();
                }, 1500);
            };
            const observer = new MutationObserver(callback);
            observer.observe(loginForm, config);
        }

    };
})();