/** * ComfyUI MechaBaby Node Search Extension * * 功能说明�? * 1. 节点名称搜索定位 - 支持节点标题和类型的搜索 * 2. 节点属性搜�?- 搜索控件名称、控件值、属性名称、属性�? * 3. 快捷键支�?- Ctrl+F 快速打开搜索对话�? * 4. 键盘导航 - 支持上下箭头键选择,Enter 跳转,Esc 关闭 * * 技术实现: * - 使用 ComfyUI Extension API (app.registerExtension) * - 访问 app.graph._nodes 获取所有节�? * - 使用 app.canvas.centerOnNode() 实现节点定位 * - 创建自定义对话框 UI 显示搜索结果 * * 依赖�? * - ComfyUI 核心 API (app, app.graph, app.canvas) * - LiteGraph API (LGraphCanvas) * * @file nodeSearch.js * @author MechaBaby * @version 1.3.2 */ import { app } from "../../../scripts/app.js"; // 多语言资源 var i18n = { 'zh-CN': { searchNodes: '搜索节点', inputPlaceholder: '输入关键词搜索节点名称、ID、属性名称或�?..', closeButton: '关闭 (Esc)', noResults: '未找到匹配的节点', foundNodes: '找到 {0} 个节点,{1} 个匹配项', nodeTitle: '节点标题', nodeType: '节点类型', nodeId: '节点ID', widget: '控件', value: '�?, property: '属�?, propertyValue: '属性�?, unknownNode: '未知节点', errorNode: '错误节点', nodeMayNotLoaded: '节点可能未正确加�?, matches: '{0} 个匹�?, moreMatches: '... 还有 {0} 个匹配项', nodeLabel: '节点: ', typeLabel: '类型: ', nodeIdLabel: 'ID: ', widgetLabel: '控件: ', valueLabel: '�? ', propertyLabel: '属�? ', propertyValueLabel: '属性�? ', nodeMayNotLoadedHint: ' | ⚠️ 节点可能未正确加�?, settings: '设置', nodeSearchSettings: '节点搜索设置', language: '语言', shortcut: '快捷�?, save: '保存', cancel: '取消', selectLanguage: '选择语言', selectShortcut: '设置快捷�?, currentShortcut: '当前快捷�? ', pressKey: '按下您想要的快捷键组�?..', invalidShortcut: '无效的快捷键,请至少包含 Ctrl/Cmd 和一个按�?, shortcutSaved: '快捷键已保存', languageSaved: '语言已保存,请刷新页�?, autoDetect: '自动检�?, chinese: '中文', english: 'English', japanese: '日本�?, korean: '한국�?, russian: 'Русский' }, 'en-US': { searchNodes: 'Search Nodes', inputPlaceholder: 'Enter keywords to search node names, IDs, properties, or values...', closeButton: 'Close (Esc)', noResults: 'No matching nodes found', foundNodes: 'Found {0} nodes, {1} matches', nodeTitle: 'Node Title', nodeType: 'Node Type', nodeId: 'Node ID', widget: 'Widget', value: 'Value', property: 'Property', propertyValue: 'Property Value', unknownNode: 'Unknown Node', errorNode: 'Error Node', nodeMayNotLoaded: 'Node may not be loaded correctly', matches: '{0} matches', moreMatches: '... {0} more matches', nodeLabel: 'Node: ', typeLabel: 'Type: ', nodeIdLabel: 'ID: ', widgetLabel: 'Widget: ', valueLabel: 'Value: ', propertyLabel: 'Property: ', propertyValueLabel: 'Property Value: ', nodeMayNotLoadedHint: ' | ⚠️ Node may not be loaded correctly', settings: 'Settings', nodeSearchSettings: 'NodeSearch Settings', language: 'Language', shortcut: 'Shortcut', save: 'Save', cancel: 'Cancel', selectLanguage: 'Select Language', selectShortcut: 'Set Shortcut', currentShortcut: 'Current Shortcut: ', pressKey: 'Press your desired shortcut combination...', invalidShortcut: 'Invalid shortcut, please include at least Ctrl/Cmd and one key', shortcutSaved: 'Shortcut saved', languageSaved: 'Language saved, please refresh the page', autoDetect: 'Auto Detect', chinese: '中文', english: 'English', japanese: '日本�?, korean: '한국�?, russian: 'Русский' }, 'ja-JP': { searchNodes: 'ノード検�?, inputPlaceholder: 'キーワードを入力してノード名、ID、プロパティ、または値を検索...', closeButton: '閉じ�?(Esc)', noResults: '一致するノードが見つかりません', foundNodes: '{0} 個のノード、{1} 個の一致が見つかりまし�?, nodeTitle: 'ノードタイト�?, nodeType: 'ノードタイプ', nodeId: 'ノードID', widget: 'ウィジェット', value: '�?, property: 'プロパテ�?, propertyValue: 'プロパティ�?, unknownNode: '不明なノード', errorNode: 'エラーノード', nodeMayNotLoaded: 'ノードが正しく読み込まれていない可能性があります', matches: '{0} 個の一�?, moreMatches: '... あと {0} 個の一�?, nodeLabel: 'ノー�? ', typeLabel: 'タイ�? ', nodeIdLabel: 'ID: ', widgetLabel: 'ウィジェット: ', valueLabel: '�? ', propertyLabel: 'プロパテ�? ', propertyValueLabel: 'プロパティ�? ', nodeMayNotLoadedHint: ' | ⚠️ ノードが正しく読み込まれていない可能性があります', settings: '設定', nodeSearchSettings: 'NodeSearch 設定', language: '言�?, shortcut: 'ショートカッ�?, save: '保存', cancel: 'キャンセ�?, selectLanguage: '言語を選択', selectShortcut: 'ショートカットを設定', currentShortcut: '現在のショートカット: ', pressKey: '希望のショートカットキーの組み合わせを押してください...', invalidShortcut: '無効なショートカットです。Ctrl/Cmdと少なくとも1つのキーを含めてください', shortcutSaved: 'ショートカットが保存されまし�?, languageSaved: '言語が保存されました。ページを更新してくださ�?, autoDetect: '自動検出', chinese: '中文', english: 'English', japanese: '日本�?, korean: '한국�?, russian: 'Русский' }, 'ko-KR': { searchNodes: '노드 검�?, inputPlaceholder: '키워드를 입력하여 노드 이름, ID, 속성 또는 값을 검�?..', closeButton: '닫기 (Esc)', noResults: '일치하는 노드�?찾을 �?없습니다', foundNodes: '{0}개의 노드, {1}개의 일치 항목�?찾았습니�?, nodeTitle: '노드 제목', nodeType: '노드 유형', nodeId: '노드 ID', widget: '위젯', value: '�?, property: '속성', propertyValue: '속성 �?, unknownNode: '�?�?없는 노드', errorNode: '오류 노드', nodeMayNotLoaded: '노드가 제대�?로드되지 않았�?�?있습니다', matches: '{0}개의 일치', moreMatches: '... {0}�?�?일치', nodeLabel: '노드: ', typeLabel: '유형: ', nodeIdLabel: 'ID: ', widgetLabel: '위젯: ', valueLabel: '�? ', propertyLabel: '속성: ', propertyValueLabel: '속성 �? ', nodeMayNotLoadedHint: ' | ⚠️ 노드가 제대�?로드되지 않았�?�?있습니다', settings: '설정', nodeSearchSettings: 'NodeSearch 설정', language: '언어', shortcut: '단축�?, save: '저�?, cancel: '취소', selectLanguage: '언어 선택', selectShortcut: '단축�?설정', currentShortcut: '현재 단축�? ', pressKey: '원하�?단축�?조합�?누르세요...', invalidShortcut: '잘못�?단축키입니다. Ctrl/Cmd와 최소 하나�?키를 포함하세�?, shortcutSaved: '단축키가 저장되었습니다', languageSaved: '언어가 저장되었습니다. 페이지�?새로고침하세�?, autoDetect: '자동 감지', chinese: '中文', english: 'English', japanese: '日本�?, korean: '한국�?, russian: 'Русский' }, 'ru-RU': { searchNodes: 'Поиск узлов', inputPlaceholder: 'Введите ключевые слова для поиска имен узлов, ID, свойств или значений...', closeButton: 'Закрыть (Esc)', noResults: 'Совпадающие узлы не найдены', foundNodes: 'Найдено {0} узлов, {1} совпадений', nodeTitle: 'Заголовок узла', nodeType: 'Тип узла', nodeId: 'ID узла', widget: 'Виджет', value: 'Значение', property: 'Свойство', propertyValue: 'Значение свойства', unknownNode: 'Неизвестный узел', errorNode: 'Ошибочный узел', nodeMayNotLoaded: 'Узел может быть загружен неправильно', matches: '{0} совпадений', moreMatches: '... еще {0} совпадений', nodeLabel: 'Узел: ', typeLabel: 'Тип: ', nodeIdLabel: 'ID: ', widgetLabel: 'Виджет: ', valueLabel: 'Значение: ', propertyLabel: 'Свойство: ', propertyValueLabel: 'Значение свойства: ', nodeMayNotLoadedHint: ' | ⚠️ Узел может быть загружен неправильно', settings: 'Настройки', nodeSearchSettings: 'NodeSearch Настройки', language: 'Язык', shortcut: 'Горячая клавиша', save: 'Сохранить', cancel: 'Отмена', selectLanguage: 'Выберите язык', selectShortcut: 'Установить горячую клавишу', currentShortcut: 'Текущая горячая клавиша: ', pressKey: 'Нажмите желаемую комбинацию клавиш...', invalidShortcut: 'Неверная горячая клавиша, пожалуйста, включите как минимум Ctrl/Cmd и одну клавишу', shortcutSaved: 'Горячая клавиша сохранена', languageSaved: 'Язык сохранен, пожалуйста, обновите страницу', autoDetect: 'Автоопределение', chinese: '中文', english: 'English', japanese: '日本�?, korean: '한국�?, russian: 'Русский' } }; // 语言代码映射(将浏览器语言代码映射到支持的语言�? var langMap = { 'zh': 'zh-CN', 'zh-CN': 'zh-CN', 'zh-TW': 'zh-CN', 'en': 'en-US', 'en-US': 'en-US', 'en-GB': 'en-US', 'ja': 'ja-JP', 'ja-JP': 'ja-JP', 'ko': 'ko-KR', 'ko-KR': 'ko-KR', 'ru': 'ru-RU', 'ru-RU': 'ru-RU' }; // 配置管理 var config = { // 获取当前语言 getLanguage: function() { var saved = localStorage.getItem('mechababy.nodeSearch.language'); if (saved && i18n[saved]) { return saved; } // 自动检测浏览器语言 var browserLang = navigator.language || navigator.userLanguage || 'en-US'; return langMap[browserLang] || langMap[browserLang.split('-')[0]] || 'en-US'; }, // 设置语言 setLanguage: function(lang) { if (i18n[lang]) { localStorage.setItem('mechababy.nodeSearch.language', lang); return true; } return false; }, // 获取当前快捷�? getShortcut: function() { var saved = localStorage.getItem('mechababy.nodeSearch.shortcut'); if (saved) { try { return JSON.parse(saved); } catch (e) { return { ctrl: true, key: 'f' }; } } return { ctrl: true, key: 'f' }; // 默认 Ctrl+F }, // 设置快捷�? setShortcut: function(shortcut) { try { localStorage.setItem('mechababy.nodeSearch.shortcut', JSON.stringify(shortcut)); return true; } catch (e) { return false; } }, // 格式化快捷键显示 formatShortcut: function(shortcut) { if (!shortcut) shortcut = this.getShortcut(); var parts = []; if (shortcut.ctrl) parts.push('Ctrl'); if (shortcut.alt) parts.push('Alt'); if (shortcut.shift) parts.push('Shift'); if (shortcut.meta) parts.push('Cmd'); if (shortcut.key) { var key = shortcut.key; if (key.length === 1) { key = key.toUpperCase(); } parts.push(key); } return parts.join('+'); } }; // 获取当前语言的文�? function t(key) { var lang = config.getLanguage(); var texts = i18n[lang] || i18n['en-US']; var text = texts[key] || i18n['en-US'][key] || key; // 简单的格式化(支持 {0}, {1} 等占位符�? if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { text = text.replace('{' + (i - 1) + '}', arguments[i]); } } return text; } // 模块级变量,用于在不�?hook 之间共享 var nodeSearchState = { searchDialog: null, searchResults: [], currentResultIndex: -1, keyboardHandlerBound: false, openSearchDialog: null, // 将在 setup 中设�? currentHandler: null, // 当前快捷键处理器 settingsDialog: null // 设置对话�? }; app.registerExtension({ name: "MechaBaby.NodeSearch", // 新的 Context Menu API hook getCanvasMenuItems: function() { var currentShortcut = config.formatShortcut(); return [ null, // separator { content: '🔍 ' + t('searchNodes') + ' (' + currentShortcut + ')', callback: function() { if (nodeSearchState.openSearchDialog) { nodeSearchState.openSearchDialog(); } } }, null, // separator { content: '⚙️ ' + t('nodeSearchSettings'), has_submenu: true, submenu: { options: [ { content: t('language'), has_submenu: true, submenu: { options: [ { content: t('autoDetect'), callback: function() { config.setLanguage(config.getLanguage()); } }, null, { content: '🇨🇳 ' + t('chinese'), callback: function() { config.setLanguage('zh-CN'); alert(t('languageSaved')); } }, { content: '🇺🇸 ' + t('english'), callback: function() { config.setLanguage('en-US'); alert(t('languageSaved')); } }, { content: '🇯🇵 ' + t('japanese'), callback: function() { config.setLanguage('ja-JP'); alert(t('languageSaved')); } }, { content: '🇰🇷 ' + t('korean'), callback: function() { config.setLanguage('ko-KR'); alert(t('languageSaved')); } }, { content: '🇷🇺 ' + t('russian'), callback: function() { config.setLanguage('ru-RU'); alert(t('languageSaved')); } } ] } }, { content: t('shortcut'), callback: function() { if (nodeSearchState.openSettingsDialog) { nodeSearchState.openSettingsDialog(); } } } ] } } ]; }, setup: function() { var searchDialog = nodeSearchState.searchDialog; var searchResults = nodeSearchState.searchResults; var currentResultIndex = nodeSearchState.currentResultIndex; var keyboardHandlerBound = nodeSearchState.keyboardHandlerBound; /** * 搜索节点(包括名称、属性名称、属性值) * @param {string} keyword - 搜索关键�? */ function searchNodes(keyword) { if (!keyword || keyword.trim() === "") { return []; } var keywordLower = keyword.toLowerCase().trim(); var results = []; // 搜索工作流中的节�? if (app.graph && app.graph._nodes) { app.graph._nodes.forEach(function(node) { // 使用 try-catch 保护,避免红色报错节点导致搜索中�? try { // 检查节点是否有效(红色报错节点可能缺少某些属性) if (!node) return; const matches = []; let nodeTitle = ""; // 安全地获取节点标�? try { nodeTitle = node.getTitle ? node.getTitle() : (node.title || node.type || ""); } catch (e) { nodeTitle = node.type || node.title || t('unknownNode'); } // 1. 搜索节点标题 if (nodeTitle && nodeTitle.toLowerCase().includes(keywordLower)) { matches.push({ type: 'node_title', name: t('nodeTitle'), value: nodeTitle, display: t('nodeLabel') + nodeTitle }); } // 2. 搜索节点类型 if (node.type && node.type.toLowerCase().includes(keywordLower)) { matches.push({ type: 'node_type', name: t('nodeType'), value: node.type, display: t('typeLabel') + node.type }); } // 3. 搜索节点ID if (node.id !== undefined && node.id !== null) { var nodeIdStr = String(node.id); if (nodeIdStr.toLowerCase().includes(keywordLower)) { matches.push({ type: 'node_id', name: t('nodeId'), value: node.id, display: t('nodeIdLabel') + nodeIdStr }); } } // 4. 搜索控件名称和值(安全访问�? try { if (node.widgets && Array.isArray(node.widgets)) { node.widgets.forEach(function(widget, index) { try { if (!widget) return; const widgetName = widget.name || ''; const widgetValue = (node.widgets_values && node.widgets_values[index] !== undefined) ? node.widgets_values[index] : (widget.value !== undefined ? widget.value : ''); // 搜索控件名称 if (widgetName && widgetName.toLowerCase().includes(keywordLower)) { matches.push({ type: 'widget_name', name: widgetName, value: widgetValue, display: t('widgetLabel') + widgetName + ' = ' + String(widgetValue).substring(0, 50) }); } // 搜索控件值(转换为字符串�? const valueStr = String(widgetValue); if (valueStr && valueStr.toLowerCase().includes(keywordLower) && widgetName) { matches.push({ type: 'widget_value', name: widgetName, value: widgetValue, display: t('valueLabel') + widgetName + ' = ' + valueStr.substring(0, 50) }); } } catch (widgetError) { // 单个控件出错不影响其他控�? console.debug("[MechaBaby NodeSearch] 搜索控件时出�?", widgetError); } }); } } catch (widgetsError) { // 控件访问出错,继续搜索其他属�? console.debug("[MechaBaby NodeSearch] 访问节点控件时出�?", widgetsError); } // 5. 搜索节点属性(安全访问�? try { if (node.properties && typeof node.properties === 'object') { Object.keys(node.properties).forEach(function(propName) { try { var propValue = node.properties[propName]; var propValueStr = String(propValue); // 搜索属性名�? if (propName && propName.toLowerCase().includes(keywordLower)) { matches.push({ type: 'property_name', name: propName, value: propValue, display: t('propertyLabel') + propName + ' = ' + propValueStr.substring(0, 50) }); } // 搜索属性�? if (propValueStr && propValueStr.toLowerCase().includes(keywordLower)) { matches.push({ type: 'property_value', name: propName, value: propValue, display: t('propertyValueLabel') + propName + ' = ' + propValueStr.substring(0, 50) }); } } catch (propError) { // 单个属性出错不影响其他属�? console.debug("[MechaBaby NodeSearch] 搜索属性时出错:", propError); } }); } } catch (propertiesError) { // 属性访问出错,继续处理 console.debug("[MechaBaby NodeSearch] 访问节点属性时出错:", propertiesError); } // 如果有匹配,添加到结�? if (matches.length > 0) { results.push({ node: node, nodeTitle: nodeTitle, nodeType: node.type || "未知类型", nodeId: node.id, matches: matches, matchCount: matches.length, isAvailableNode: false // 工作流中的节�? }); } } catch (nodeError) { // 节点处理出错,记录但继续搜索其他节点 const nodeTypeStr = (node && node.type) ? node.type : "未知"; console.debug("[MechaBaby NodeSearch] 搜索节点时出错(可能是未安装的节点):", nodeError, nodeTypeStr); // 即使节点出错,也尝试搜索节点类型(如果可用) if (node && node.type) { try { const nodeType = node.type; if (nodeType.toLowerCase().includes(keywordLower)) { results.push({ node: node, nodeTitle: nodeType + ' (' + t('errorNode') + ')', nodeType: nodeType, nodeId: node.id, matches: [{ type: 'node_type', name: t('nodeType'), value: nodeType, display: t('typeLabel') + nodeType + ' (' + t('nodeMayNotLoaded') + ')' }], matchCount: 1, isAvailableNode: false, hasError: true // 标记为有错误的节�? }); } } catch (e) { // 完全无法处理,跳�? } } } }); } return results; } /** * 跳转到节点并高亮 */ function jumpToNode(node, matchIndex) { if (matchIndex === undefined) { matchIndex = 0; } if (!node) return; // 跳转到节�? app.canvas.centerOnNode(node); // 选中节点 app.canvas.selectNode(node); // 添加金黄色闪烁高亮效�? highlightNode(node); } /** * 高亮闪烁节点(金黄色效果�? */ function highlightNode(node) { if (!node) return; // 保存原始颜色 var originalColor = node.color; var originalBgColor = node.bgcolor; // 金黄色高亮颜�? var highlightColor = "#FFD700"; var highlightBgColor = "#4a3d00"; var flashCount = 0; var maxFlashes = 6; // 闪烁3次(6次切换) var flashInterval = 150; // 每次闪烁间隔150ms function flash() { if (flashCount >= maxFlashes) { // 恢复原始颜色 node.color = originalColor; node.bgcolor = originalBgColor; app.canvas.setDirty(true, true); return; } if (flashCount % 2 === 0) { // 高亮 node.color = highlightColor; node.bgcolor = highlightBgColor; } else { // 恢复 node.color = originalColor; node.bgcolor = originalBgColor; } app.canvas.setDirty(true, true); flashCount++; setTimeout(flash, flashInterval); } // 开始闪�? flash(); } /** * 创建搜索对话�? */ function createSearchDialog() { if (searchDialog) { return searchDialog; } const dialog = document.createElement('div'); dialog.id = 'mechababy-node-search-dialog'; dialog.style.cssText = 'position: fixed;' + 'top: 50%;' + 'left: 50%;' + 'transform: translate(-50%, -50%);' + 'background: #2a2a2a;' + 'border: 2px solid #4a4a4a;' + 'border-radius: 8px;' + 'padding: 20px;' + 'z-index: 10000;' + 'min-width: 500px;' + 'max-width: 700px;' + 'max-height: 600px;' + 'display: flex;' + 'flex-direction: column;' + 'box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);' + "font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;"; // 标题 const title = document.createElement('div'); title.textContent = '🔍 ' + t('searchNodes'); title.style.cssText = 'font-size: 18px;' + 'font-weight: bold;' + 'color: #e0e0e0;' + 'margin-bottom: 15px;' + 'border-bottom: 1px solid #4a4a4a;' + 'padding-bottom: 10px;'; dialog.appendChild(title); // 搜索输入�? const inputContainer = document.createElement('div'); inputContainer.style.cssText = 'margin-bottom: 15px;'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = t('inputPlaceholder'); input.style.cssText = 'width: 100%;' + 'padding: 10px;' + 'background: #1a1a1a;' + 'border: 1px solid #4a4a4a;' + 'border-radius: 4px;' + 'color: #e0e0e0;' + 'font-size: 14px;' + 'box-sizing: border-box;'; input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); if (searchResults.length > 0 && currentResultIndex >= 0) { var result = searchResults[currentResultIndex]; jumpToNode(result.node); closeDialog(); } } else if (e.key === 'ArrowDown') { e.preventDefault(); if (currentResultIndex < searchResults.length - 1) { currentResultIndex++; updateResultsList(); } } else if (e.key === 'ArrowUp') { e.preventDefault(); if (currentResultIndex > 0) { currentResultIndex--; updateResultsList(); } } else if (e.key === 'Escape') { closeDialog(); } }); input.addEventListener('input', function(e) { var keyword = e.target.value; searchResults = searchNodes(keyword); currentResultIndex = searchResults.length > 0 ? 0 : -1; updateResultsList(); }); inputContainer.appendChild(input); dialog.appendChild(inputContainer); // 结果列表容器 const resultsContainer = document.createElement('div'); resultsContainer.id = 'search-results-container'; resultsContainer.style.cssText = 'flex: 1;' + 'overflow-y: auto;' + 'max-height: 400px;' + 'border: 1px solid #4a4a4a;' + 'border-radius: 4px;' + 'background: #1a1a1a;'; dialog.appendChild(resultsContainer); // 提示信息 const info = document.createElement('div'); info.id = 'search-info'; info.style.cssText = 'margin-top: 10px;' + 'font-size: 12px;' + 'color: #888;' + 'text-align: center;'; dialog.appendChild(info); // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.textContent = t('closeButton'); closeBtn.style.cssText = 'margin-top: 10px;' + 'padding: 8px 16px;' + 'background: #4a4a4a;' + 'border: none;' + 'border-radius: 4px;' + 'color: #e0e0e0;' + 'cursor: pointer;' + 'font-size: 14px;'; closeBtn.addEventListener('click', closeDialog); closeBtn.addEventListener('mouseenter', function() { closeBtn.style.background = '#5a5a5a'; }); closeBtn.addEventListener('mouseleave', function() { closeBtn.style.background = '#4a4a4a'; }); dialog.appendChild(closeBtn); // 更新结果列表 function updateResultsList() { const container = resultsContainer; container.innerHTML = ''; if (searchResults.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.textContent = t('noResults'); emptyMsg.style.cssText = 'padding: 20px;' + 'text-align: center;' + 'color: #888;'; container.appendChild(emptyMsg); info.textContent = ''; return; } var totalMatches = 0; for (var i = 0; i < searchResults.length; i++) { totalMatches += searchResults[i].matchCount; } info.textContent = t('foundNodes', searchResults.length, totalMatches); for (var idx = 0; idx < searchResults.length; idx++) { (function(index) { var result = searchResults[index]; var item = document.createElement('div'); item.setAttribute('data-result-index', index); item.style.cssText = 'padding: 12px; border-bottom: 1px solid #3a3a3a; cursor: pointer; background: ' + (index === currentResultIndex ? '#3a3a3a' : 'transparent') + '; transition: background 0.2s;'; item.addEventListener('mouseenter', function() { if (index !== currentResultIndex) { item.style.background = '#333'; } }); item.addEventListener('mouseleave', function() { if (index !== currentResultIndex) { item.style.background = 'transparent'; } }); item.addEventListener('click', function() { currentResultIndex = index; updateResultsList(); if (result.hasError) { // 错误节点,尝试跳�? if (result.node) { try { jumpToNode(result.node); } catch (e) { alert(t('nodeMayNotLoaded') + ': "' + result.nodeTitle + '"'); } } } else { // 跳转到节�? jumpToNode(result.node); closeDialog(); } }); // 节点标题 var title = document.createElement('div'); var statusBadge = result.hasError ? ' [' + t('errorNode') + ']' : ''; title.textContent = result.nodeTitle + statusBadge + ' (' + t('matches', result.matchCount) + ')'; title.style.cssText = 'font-weight: bold; color: ' + (result.hasError ? '#ff4a4a' : '#4a9eff') + '; margin-bottom: 5px; font-size: 14px;'; item.appendChild(title); // 节点类型 var type = document.createElement('div'); var typeText = t('typeLabel') + result.nodeType; if (result.hasError) { typeText += t('nodeMayNotLoadedHint'); } type.textContent = typeText; type.style.cssText = 'color: ' + (result.hasError ? '#ff8888' : '#888') + '; font-size: 12px; margin-bottom: 8px;'; item.appendChild(type); // 匹配项列表(最多显�?个) var matchesList = document.createElement('div'); var displayMatches = result.matches.slice(0, 3); for (var j = 0; j < displayMatches.length; j++) { var match = displayMatches[j]; var matchItem = document.createElement('div'); matchItem.textContent = ' �?' + match.display; matchItem.style.cssText = 'color: #aaa; font-size: 12px; margin-left: 10px; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'; matchesList.appendChild(matchItem); } if (result.matches.length > 3) { var more = document.createElement('div'); more.textContent = ' ' + t('moreMatches', result.matches.length - 3); more.style.cssText = 'color: #666; font-size: 11px; margin-left: 10px; font-style: italic;'; matchesList.appendChild(more); } item.appendChild(matchesList); container.appendChild(item); })(idx); } // 自动滚动到当前选中的项 if (currentResultIndex >= 0 && currentResultIndex < searchResults.length) { setTimeout(function() { var selectedItem = container.querySelector('[data-result-index="' + currentResultIndex + '"]'); if (selectedItem) { selectedItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }, 0); } } function closeDialog() { if (dialog.parentNode) { dialog.parentNode.removeChild(dialog); } searchDialog = null; searchResults = []; currentResultIndex = -1; } // 点击外部关闭 dialog.addEventListener('click', function(e) { if (e.target === dialog) { closeDialog(); } }); // 初始�? updateResultsList(); return dialog; } /** * 打开搜索对话�? */ function openSearchDialog() { if (!searchDialog) { searchDialog = createSearchDialog(); document.body.appendChild(searchDialog); } // 每次打开都聚焦输入框 setTimeout(function() { var input = searchDialog.querySelector('input'); if (input) { input.focus(); input.select(); } }, 50); } // 保存函数引用到模块级变量,供 getCanvasMenuItems hook 使用 nodeSearchState.openSearchDialog = openSearchDialog; /** * 检查快捷键是否匹配 */ function checkShortcutMatch(e, shortcut) { if (!shortcut) return false; var ctrlMatch = shortcut.ctrl ? (e.ctrlKey || e.metaKey) : (!e.ctrlKey && !e.metaKey); var altMatch = shortcut.alt ? e.altKey : !e.altKey; var shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey; var metaMatch = shortcut.meta ? e.metaKey : !e.metaKey; var keyMatch = shortcut.key && (e.key === shortcut.key || e.key === shortcut.key.toLowerCase() || e.key === shortcut.key.toUpperCase()); return ctrlMatch && altMatch && shiftMatch && metaMatch && keyMatch; } /** * 绑定快捷键监听器(支持自定义快捷键) */ function bindKeyboardShortcut() { // 移除旧的监听�? if (nodeSearchState.currentHandler) { document.removeEventListener('keydown', nodeSearchState.currentHandler, true); window.removeEventListener('keydown', nodeSearchState.currentHandler, true); if (app.canvas && app.canvas.canvas) { app.canvas.canvas.removeEventListener('keydown', nodeSearchState.currentHandler, true); } } var shortcut = config.getShortcut(); var handler = function(e) { // 检查是否匹配自定义快捷�? if (checkShortcutMatch(e, shortcut)) { // 如果输入框有焦点,不拦截(让用户可以在搜索框中输入) var activeElement = document.activeElement; if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { // 如果焦点在搜索对话框的输入框中,不拦�? if (searchDialog && searchDialog.contains(activeElement)) { return; } } // 阻止默认行为(浏览器搜索�? e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); openSearchDialog(); return false; } }; // 保存处理器引�? nodeSearchState.currentHandler = handler; // 在多个地方绑定,确保能捕获到 // 1. document 级别(捕获阶段,优先级最高) document.addEventListener('keydown', handler, true); // 2. window 级别 window.addEventListener('keydown', handler, true); // 3. 画布级别(如果可用) if (app.canvas) { if (app.canvas.canvas) { app.canvas.canvas.addEventListener('keydown', handler, true); } // 等待画布完全加载 setTimeout(function() { if (app.canvas && app.canvas.canvas) { app.canvas.canvas.addEventListener('keydown', handler, true); } }, 500); } keyboardHandlerBound = true; } /** * 创建设置对话�? */ function createSettingsDialog() { if (nodeSearchState.settingsDialog) { return nodeSearchState.settingsDialog; } var dialog = document.createElement('div'); dialog.id = 'mechababy-node-search-settings'; dialog.style.cssText = 'position: fixed;' + 'top: 50%;' + 'left: 50%;' + 'transform: translate(-50%, -50%);' + 'background: #2a2a2a;' + 'border: 2px solid #4a4a4a;' + 'border-radius: 8px;' + 'padding: 20px;' + 'z-index: 10001;' + 'min-width: 400px;' + 'max-width: 500px;' + 'box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);' + "font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;"; // 标题 var title = document.createElement('div'); title.textContent = '⚙️ ' + t('settings'); title.style.cssText = 'font-size: 18px;' + 'font-weight: bold;' + 'color: #e0e0e0;' + 'margin-bottom: 20px;' + 'border-bottom: 1px solid #4a4a4a;' + 'padding-bottom: 10px;'; dialog.appendChild(title); // 快捷键设�? var shortcutSection = document.createElement('div'); shortcutSection.style.cssText = 'margin-bottom: 20px;'; var shortcutLabel = document.createElement('div'); shortcutLabel.textContent = t('shortcut') + ':'; shortcutLabel.style.cssText = 'color: #e0e0e0; margin-bottom: 8px; font-size: 14px;'; shortcutSection.appendChild(shortcutLabel); var shortcutDisplay = document.createElement('div'); var currentShortcut = config.getShortcut(); shortcutDisplay.textContent = t('currentShortcut') + config.formatShortcut(currentShortcut); shortcutDisplay.style.cssText = 'color: #4a9eff; margin-bottom: 10px; font-size: 13px; padding: 8px; background: #1a1a1a; border-radius: 4px;'; shortcutSection.appendChild(shortcutDisplay); var shortcutInput = document.createElement('input'); shortcutInput.type = 'text'; shortcutInput.placeholder = t('pressKey'); shortcutInput.readOnly = true; shortcutInput.style.cssText = 'width: 100%;' + 'padding: 10px;' + 'background: #1a1a1a;' + 'border: 1px solid #4a4a4a;' + 'border-radius: 4px;' + 'color: #e0e0e0;' + 'font-size: 14px;' + 'box-sizing: border-box;'; var capturing = false; var capturedShortcut = null; shortcutInput.addEventListener('focus', function() { if (!capturing) { capturing = true; shortcutInput.placeholder = t('pressKey'); shortcutInput.value = ''; capturedShortcut = null; } }); shortcutInput.addEventListener('keydown', function(e) { if (!capturing) return; e.preventDefault(); e.stopPropagation(); // 忽略某些特殊�? if (e.key === 'Tab' || e.key === 'Escape' || e.key === 'Enter') { return; } // 至少需�?Ctrl/Cmd 和一个按�? if (!e.ctrlKey && !e.metaKey && !e.altKey) { shortcutInput.value = t('invalidShortcut'); return; } var shortcut = { ctrl: e.ctrlKey || e.metaKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey, key: e.key }; capturedShortcut = shortcut; shortcutInput.value = config.formatShortcut(shortcut); }); shortcutSection.appendChild(shortcutInput); dialog.appendChild(shortcutSection); // 按钮 var buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;'; var saveBtn = document.createElement('button'); saveBtn.textContent = t('save'); saveBtn.style.cssText = 'padding: 8px 16px;' + 'background: #4a9eff;' + 'border: none;' + 'border-radius: 4px;' + 'color: #fff;' + 'cursor: pointer;' + 'font-size: 14px;'; saveBtn.addEventListener('click', function() { if (capturedShortcut) { // 验证快捷�? if (!capturedShortcut.key || (!capturedShortcut.ctrl && !capturedShortcut.meta && !capturedShortcut.alt)) { alert(t('invalidShortcut')); return; } config.setShortcut(capturedShortcut); bindKeyboardShortcut(); // 重新绑定快捷�? alert(t('shortcutSaved')); closeSettingsDialog(); } else { closeSettingsDialog(); } }); var cancelBtn = document.createElement('button'); cancelBtn.textContent = t('cancel'); cancelBtn.style.cssText = 'padding: 8px 16px;' + 'background: #4a4a4a;' + 'border: none;' + 'border-radius: 4px;' + 'color: #e0e0e0;' + 'cursor: pointer;' + 'font-size: 14px;'; cancelBtn.addEventListener('click', closeSettingsDialog); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(saveBtn); dialog.appendChild(buttonContainer); function closeSettingsDialog() { if (dialog.parentNode) { dialog.parentNode.removeChild(dialog); } nodeSearchState.settingsDialog = null; capturing = false; capturedShortcut = null; } // 点击外部关闭 dialog.addEventListener('click', function(e) { if (e.target === dialog) { closeSettingsDialog(); } }); nodeSearchState.settingsDialog = dialog; return dialog; } /** * 打开设置对话�? */ function openSettingsDialog() { if (!nodeSearchState.settingsDialog) { nodeSearchState.settingsDialog = createSettingsDialog(); document.body.appendChild(nodeSearchState.settingsDialog); } // 聚焦快捷键输入框 setTimeout(function() { var input = nodeSearchState.settingsDialog.querySelector('input'); if (input) { input.focus(); } }, 50); } // 保存函数引用到模块级变量 nodeSearchState.openSettingsDialog = openSettingsDialog; // 立即绑定快捷�? bindKeyboardShortcut(); // 延迟再次绑定,确保在所有扩展加载后 setTimeout(function() { bindKeyboardShortcut(); }, 1000); // 右键菜单通过 getCanvasMenuItems hook 添加(新�?Context Menu API�? var currentShortcut = config.formatShortcut(); console.log("[MechaBaby NodeSearch] 扩展已加�?- �?" + currentShortcut + " 打开搜索"); console.log("[MechaBaby NodeSearch] 当前语言: " + config.getLanguage()); } });