米家中枢极客版助手

登录极客版页面后,自动开启助手插件,显示设备与规则的关系,方便查看规则与设备的对应关系,支持快捷键折叠/展开,关闭,适应画布布局,设备高亮,日志高亮,自动适应画布、设置规则列表布局样式等功能。

// ==UserScript==
// @name         米家中枢极客版助手
// @namespace    http://tampermonkey.net/
// @version      v0.8.15
// @description  登录极客版页面后,自动开启助手插件,显示设备与规则的关系,方便查看规则与设备的对应关系,支持快捷键折叠/展开,关闭,适应画布布局,设备高亮,日志高亮,自动适应画布、设置规则列表布局样式等功能。
// @author       王丰,sk163
// @license      MIT
// @match        http://*/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==
/**
 * v0.8.8更新:
 * 1. 更改了插件名称
 * 2、增加了快捷键,Ctrl/Command+E折叠/展开,Ctrl/Command+Q关闭,Ctrl/Command+B适应画布布局
 * 3、自动画布布局修改为仅在初次进入规则编排页面或激活编排页面时触发
 * 4、极客版登录后自动启动插件,无需再点击设备列表激活
 * 5、修正了一些已知问题
 *
 * v0.8.9更新:
 * 1、修改了日志高亮逻辑,优化了执行效率,提升了性能,减少无用的循环
 * 2、参考了米家自动化极客版样式优化(感谢原作者:lqs1848,https://greasyfork.org/zh-CN/scripts/495833),增加了规则列表样式设置选项,可选择每行显示1-5条规则
 * 3、增加了自动折叠窗口的选项
 * 4、修正了一些已知问题
 *
 * v0.8.10更新:
 * 1、修复了窗口过小的问题
 * 2、取消了规则列表最小宽度限制
 *
 * v0.8.11更新:
 * 1、规则列表换行显示
 *
 * v0.8.12更新:
 * 1、增加了快捷键Ctrl+W,关闭当前的画布
 * 2、将快捷键说明增加到了原生的【使用指南】中
 *
 * v0.8.13更新:
 * 1、修复了一行显示多列时,由于规则名称过长导致显示错位的问题
 *
 * v0.8.14更新:
 * 1、增加刷新助手按钮,同时增加了快捷键Ctrl+R。查看所有的快捷键,可在极客版画布的【使用指南】中进行查看。
 * 2、优化了自动化规则列表在一行多列下的显示效果。
 * 3、修正了一些已知问题
 *
 * v0.8.15更新:
 * 1、修复自动化列表在多列显示时,筛选格式混乱问题(作者:lqs1848)
 * 2、修复自动化列表在多列显示时,第一个自动化显示错位问题(作者:lqs1848)
 */
(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 defaultColor='#43ad7f7f'
    let defaultWindowWidth=1120;
    let defaultWindowHeight=600;
    let defaultRuleStyle='4';
    let minWindowWidth=400;
    let minWindowHeight=100;
    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 ruleStyle = GM_getValue("ruleStyle");

    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 {
        windowWidth = parseInt(windowWidth, 10) < minWindowWidth ? minWindowWidth : parseInt(windowWidth, 10);
    }
    if (windowHeight === undefined || windowHeight === null || windowHeight === "" || isNaN(windowHeight)) {
        windowHeight = defaultWindowHeight;
    } else {
        windowHeight = parseInt(windowHeight, 10) < minWindowHeight ? minWindowHeight : parseInt(windowHeight, 10)
    }
    if (ruleStyle === undefined || ruleStyle === null || ruleStyle === "") {
        ruleStyle = defaultRuleStyle;
    }

    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 {
            isInit=true;
            const devListResponse = await callAPI('getDevList');
            const devList = devListResponse.devList;
            const roomNames = Array.from(new Set(Object.values(devList).map(device => device.roomName)));

            const ruleList = await callAPI('getGraphList');
            let devRuleMap = {};
            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 cards = new Set(content.nodes.map(n => {
                    return (n.props && n.cfg) ? {did: n.props.did, oriId: n.cfg.oriId} : undefined;
                }).filter(card => card !== undefined));

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

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

            const result = Object.fromEntries(
                Object.entries(devRuleMap).map(([k, v]) => [
                    k,
                    {
                        device: {
                            name: devList[k]?.name ?? `did: ${k}`,
                            roomName: devList[k]?.roomName ?? `未知`
                        },
                        rules: v.map(r => {
                            const rule = ruleList.find(rr => rr.id === r.ruleId);
                            return {
                                id: r.ruleId,
                                cardIds: r.cardIds,
                                totalCardNum: r.totalCardNum,
                                name: rule ? rule.userData.name : 'Unknown'
                            };
                        })
                    }
                ])
            );

            const container = document.createElement('div');
            container.id = 'device-rule-map';
            container.style.position = 'fixed';
            container.style.top = '10px';
            container.style.right = '10px';
            container.style.width = windowWidth+'px';
            container.style.height = windowHeight+'px';
            container.style.overflowY = 'scroll';
            container.style.backgroundColor = 'white';
            container.style.border = '1px solid #ccc';
            container.style.paddingTop = '45px';
            container.style.zIndex = 10000;
            container.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';

            const topBar = document.createElement('div');
            topBar.id = 'topBar';
            topBar.style.position = 'fixed';
            topBar.style.top = '0';
            topBar.style.right = '10px';
            topBar.style.width = windowWidth+'px';
            topBar.style.height = '38px';
            topBar.style.backgroundColor = 'white';
            topBar.style.zIndex = 10001;
            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 collapseButton = document.createElement('button');
            collapseButton.id="collapseButton";
            collapseButton.textContent = '折叠';
            collapseButton.title = '快捷键为Ctrl+E';
            collapseButton.onclick = () => {
                handleCollapseBtnClick();
            };

            const closeButton = document.createElement('button');
            closeButton.id="closeButton";
            closeButton.title = '快捷键为Ctrl+Q';
            closeButton.textContent = '关闭';
            closeButton.onclick = () => {
                document.body.removeChild(container);
                isInit=false;
            }

            const refreshButton = document.createElement('button');
            refreshButton.id="refreshButton";
            refreshButton.title = '快捷键为Ctrl+R';
            refreshButton.textContent = '刷新';
            refreshButton.onclick = () => {
                document.body.removeChild(container);
                isInit=false;
                executeScript();
            }

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

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

            const table = document.createElement('table');
            table.border = '1';
            table.cellSpacing = '0';
            table.cellPadding = '5';
            table.style.width = '100%';

            const thead = document.createElement('thead');
            const headerRow = document.createElement('tr');
            const roomHeader = document.createElement('th');
            const deviceHeader = document.createElement('th');
            const ruleHeader = document.createElement('th');

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

            const updateSortMarkers = () => {
                roomHeader.innerHTML = `房间 ${roomSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                deviceHeader.innerHTML = `设备 ${deviceSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                ruleHeader.innerHTML = `规则 ${ruleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
            };

            roomHeader.textContent = '房间';
            roomHeader.style.textWrap= 'nowrap';
            deviceHeader.textContent = '设备';
            deviceHeader.style.textWrap = 'nowrap';
            ruleHeader.textContent = '规则';

            roomHeader.onclick = () => {
                roomSortOrder = roomSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(0, roomSortOrder);
                updateSortMarkers();
            };
            deviceHeader.onclick = () => {
                deviceSortOrder = deviceSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(1, deviceSortOrder);
                updateSortMarkers();
            };
            ruleHeader.onclick = () => {
                ruleSortOrder = ruleSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(2, ruleSortOrder);
                updateSortMarkers();
            };

            headerRow.appendChild(roomHeader);
            headerRow.appendChild(deviceHeader);
            headerRow.appendChild(ruleHeader);
            thead.appendChild(headerRow);
            table.appendChild(thead);

            const roomFilterSelect = document.createElement('select');
            roomFilterSelect.style.marginBottom = '10px';
            roomFilterSelect.style.height = '28px';
            roomFilterSelect.style.borderStyle = 'solid';
            roomFilterSelect.style.borderWidth = '1px';
            roomFilterSelect.innerHTML = `<option value="">所有房间</option>` + roomNames.map(room => `<option value="${room}">${room}</option>`).join('');
            roomFilterSelect.onchange = () => {
                filterTable(roomFilterSelect.value,deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(roomFilterSelect);

            const deviceFilterInput = document.createElement('input');
            deviceFilterInput.type = 'text';
            deviceFilterInput.placeholder = '设备筛选';
            deviceFilterInput.style.width = '100px';
            deviceFilterInput.style.marginBottom = '10px';
            deviceFilterInput.style.marginLeft = '10px';
            deviceFilterInput.style.height = '28px';
            deviceFilterInput.style.borderStyle = 'solid';
            deviceFilterInput.style.borderWidth = '1px';
            deviceFilterInput.oninput = () => {
                filterTable(roomFilterSelect.value,deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(deviceFilterInput);

            const ruleFilterInput = document.createElement('input');
            ruleFilterInput.type = 'text';
            ruleFilterInput.placeholder = '规则筛选';
            ruleFilterInput.style.width = '100px';
            ruleFilterInput.style.marginBottom = '10px';
            ruleFilterInput.style.marginLeft = '10px';
            ruleFilterInput.style.height = '28px';
            ruleFilterInput.style.borderStyle = 'solid';
            ruleFilterInput.style.borderWidth = '1px';
            ruleFilterInput.oninput = () => {
                filterTable(roomFilterSelect.value,deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(ruleFilterInput);

            const widthInput = document.createElement('input');
            widthInput.type = 'text';
            widthInput.placeholder = windowWidth+'px';
            widthInput.style.width = '60px';
            widthInput.style.marginBottom = '10px';
            widthInput.style.marginLeft = '10px';
            widthInput.style.height = '28px';
            widthInput.style.borderStyle = 'solid';
            widthInput.style.borderWidth = '1px';
            widthInput.onchange = () => {
                windowWidth = parseInt(widthInput.value, 10) < minWindowWidth ? minWindowWidth : parseInt(widthInput.value, 10);
                GM_setValue("windowWidth", windowWidth);
                container.style.width = windowWidth + 'px';
                topBar.style.width = windowWidth + 'px';
            };
            const spanW = document.createElement('span');
            spanW.textContent = '宽度:';
            spanW.style.marginLeft = '10px';
            container.appendChild(spanW);
            container.appendChild(widthInput);

            const heightInput = document.createElement('input');
            heightInput.type = 'text';
            heightInput.placeholder = windowHeight+'px';
            heightInput.style.width = '60px';
            heightInput.style.marginBottom = '10px';
            heightInput.style.marginLeft = '10px';
            heightInput.style.height = '28px';
            heightInput.style.borderStyle = 'solid';
            heightInput.style.borderWidth = '1px';
            heightInput.onchange = () => {
                windowHeight = parseInt(heightInput.value, 10) < minWindowHeight ? minWindowHeight : parseInt(heightInput.value, 10);
                GM_setValue("windowHeight", windowHeight);
                container.style.height = windowHeight + 'px';
            };
            const spanH = document.createElement('span');
            spanH.textContent = '高度:';
            spanH.style.marginLeft = '10px';
            container.appendChild(spanH);
            container.appendChild(heightInput);

            const ruleStyleSelect = document.createElement('select');
            ruleStyleSelect.style.marginBottom = '10px';
            ruleStyleSelect.style.height = '28px';
            ruleStyleSelect.style.marginLeft = '10px';
            ruleStyleSelect.style.borderStyle = 'solid';
            ruleStyleSelect.style.borderWidth = '1px';
            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';
            container.appendChild(spanS);
            container.appendChild(ruleStyleSelect);
            ruleStyleSelect.value=ruleStyle;
            changeRuleListStyle(ruleStyle);

            const colorInput = document.createElement('input');
            colorInput.type = 'text';
            colorInput.placeholder=defaultColor;
            colorInput.style.width = '80px';
            colorInput.style.marginBottom = '10px';
            colorInput.style.marginLeft = '10px';
            colorInput.style.height = '28px';
            colorInput.style.borderStyle = 'solid';
            colorInput.style.borderWidth = '1px';
            colorInput.oninput = () => {
                backgroundColor = colorInput.value;
                GM_setValue("backgroundColor", backgroundColor);
            };
            const spanC = document.createElement('span');
            spanC.textContent = '卡片颜色:';
            spanC.style.marginLeft = '10px';
            container.appendChild(spanC);
            container.appendChild(colorInput);

            const logLabel = document.createElement('label');
            logLabel.htmlFor = 'highlightLogCheck';
            logLabel.appendChild(document.createTextNode('日志高亮'));
            logLabel.style.marginBottom = '10px';
            logLabel.style.marginLeft = '10px';
            container.appendChild(logLabel);
            const highlightLogCheck = document.createElement('input');
            highlightLogCheck.type = 'checkbox';
            highlightLogCheck.id = 'highlightLogCheck';
            highlightLogCheck.checked=enableEnhancedDisplayLog;
            highlightLogCheck.style.marginLeft = '2px';
            highlightLogCheck.onchange=function() {
                enableEnhancedDisplayLog = highlightLogCheck.checked;
                GM_setValue("enableEnhancedDisplayLog", enableEnhancedDisplayLog);
            };
            container.appendChild(highlightLogCheck);

            const fitLabel = document.createElement('label');
            fitLabel.htmlFor = 'autoFitCheck';
            fitLabel.appendChild(document.createTextNode('自动画布'));
            fitLabel.style.marginBottom = '10px';
            fitLabel.style.marginLeft = '10px';
            container.appendChild(fitLabel);
            const autoFitCheck = document.createElement('input');
            autoFitCheck.type = 'checkbox';
            autoFitCheck.id = 'autoFitCheck';
            autoFitCheck.checked=enableAutoFitContent;
            autoFitCheck.style.marginLeft = '2px';
            autoFitCheck.onchange=function() {
                enableAutoFitContent = autoFitCheck.checked;
                GM_setValue("enableAutoFitContent", enableAutoFitContent);
            };
            container.appendChild(autoFitCheck);

            const autoCollapseLabel = document.createElement('label');
            autoCollapseLabel.htmlFor = 'autoCollapseCheck';
            autoCollapseLabel.appendChild(document.createTextNode('自动折叠'));
            autoCollapseLabel.style.marginBottom = '10px';
            autoCollapseLabel.style.marginLeft = '10px';
            container.appendChild(autoCollapseLabel);
            const autoCollapseCheck = document.createElement('input');
            autoCollapseCheck.type = 'checkbox';
            autoCollapseCheck.id = 'autoCollapseCheck';
            autoCollapseCheck.checked=enableAutoCollapseCheck;
            autoCollapseCheck.style.marginLeft = '2px';
            autoCollapseCheck.onchange=function() {
                enableAutoCollapseCheck = autoCollapseCheck.checked;
                GM_setValue("enableAutoCollapseCheck", enableAutoCollapseCheck);
                autoCollapse();
            };
            container.appendChild(autoCollapseCheck);

            const tbody = document.createElement('tbody');
            Object.entries(result).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 host = window.location.host;
                let sequence = 0;
                rules.forEach(rule => {
                    const link = document.createElement('a');
                    link.href = `http://${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;
                    };
                    ruleCell.appendChild(link);
                    ruleCell.appendChild(document.createElement('br'));
                });
                ruleCell.removeChild(ruleCell.lastChild);
                row.appendChild(roomCell);
                row.appendChild(deviceCell);
                row.appendChild(ruleCell);
                tbody.appendChild(row);
            });
            table.appendChild(tbody);

            container.appendChild(topBar);
            container.appendChild(table);
            document.body.appendChild(container);

            function sortTable(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));
            }

            updateSortMarkers();

            function filterTable(roomName,deviceKeyword, ruleKeyword) {
                const rows = Array.from(tbody.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';
                    }
                });
            }
            autoCollapse();
        } catch (error) {
            isInit=false;
            console.error('调用 API 时出错:', error);
        }
    };

    const selectDevices = async () => {
        await sleep(1000);
        const cardIds = selectCardIds.split(',');
        for (const cardId of cardIds) {
            if (cardId.trim() !== '') {
                let targetElement = document.querySelector("#" + cardId.trim() + " > div > div");
                if (targetElement) {
                    targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
                }
            }
        }
        selectCardIds = '';
    };

    function handleCollapseBtnClick() {
        const container = document.getElementById('device-rule-map');
        if (container) {
            if (container.style.height === windowHeight + 'px') {
                collapse();
            }else{
                expand();
            }
        }
    }
    function collapse() {
        const container = document.getElementById('device-rule-map');
        if (container) {
            const collapseButton = document.getElementById('collapseButton');
            const topBar = document.getElementById('topBar');
            topBar.style.width = '365px';
            container.style.height = '0px';
            container.style.width = '0px';
            collapseButton.textContent = '展开';
        }
    }

    function expand() {
        const container = document.getElementById('device-rule-map');
        if (container) {
            const collapseButton = document.getElementById('collapseButton');
            const topBar = document.getElementById('topBar');
            topBar.style.width = windowWidth + 'px';
            container.style.width = windowWidth + 'px';
            container.style.height = windowHeight + 'px';
            collapseButton.textContent = '折叠';
        }
    }

    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 helpMiddle = document.querySelector('.help-modal-middle');
        const middleContent = `
        <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>
        `;
        helpMiddle.insertAdjacentHTML('beforeend', middleContent);
        const helpRight = document.querySelector('.help-modal-right');
        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) {
        let percentage= 100 / count -0.2;
        GM_addStyle('.rule-list-item {width: calc('+percentage+'% - 10px);margin: 5px;}'+
                    '.rule-list-head {width: 100%;} ' +
            '.rule-list {display: flex;flex-wrap: wrap;align-content: flex-start;} ' +
            '.rule-list-item-left {width: 60%} ' +
            '.rule-tag-list {width: 150%;flex-wrap: wrap;} ' +
            '.action {margin-left: 5px !important;} ' +
            '.rule-list-item {margin: 5px !important;} ' +
            '.rule-list-item .rule-list-item-left-content {display: block; white-space: nowrap;} ' +
            '.rule-tag-list .rule-tag-list-item {margin-left: 0px;margin-right: 4px;} ' +
            '.rule-list-item .rule-list-item-left p {white-space:break-spaces}')
    }
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    function isMiJiaJiKePage() {
        return document.title === "米家自动化极客版" && !document.querySelector('.pin-form') && editor;
    }
    function handleUrlChange() {
        if (isMiJiaJiKePage()) {
            executeScript();
            if (window.location.hash.match(/^#\/graph\/.*/g)) {
                selectDevices();
                setTimeout(function () {
                    autoFitContent();
                    enhancedDisplayLog();
                    autoCollapse();
                    addShortcutHelp();
                }, 500);
            }
        }
    }
    //页面变化
    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();
                }, 500);
            };
            const observer = new MutationObserver(callback);
            observer.observe(loginForm, config);
        }

    };
})();