/** * 提示词小助手核心类 * 统一管理小助手的生命周期、实例创建、UI交互等功能 */ import { app } from "../../../../scripts/app.js"; import { logger } from '../utils/logger.js'; import { FEATURES } from "../services/features.js"; import { HistoryManager } from "./history.js"; import { TagManager } from "./tag.js"; import { HistoryCacheService, TagCacheService, TranslateCacheService, CACHE_CONFIG, CacheService } from "../services/cache.js"; import { EventManager } from "../utils/eventManager.js"; import { ResourceManager } from "../utils/resourceManager.js"; import { UIToolkit } from "../utils/UIToolkit.js"; import { PromptFormatter } from "../utils/promptFormatter.js"; import { APIService } from "../services/api.js"; // import { CLIP_NODE_TYPES } from "../services/interceptor.js"; import { buttonMenu } from "../services/btnMenu.js"; import { rulesConfigManager } from "./rulesConfigManager.js"; /** * 更新指定CLIP编码器节点的自动翻译指示器状态 * 使用从interceptor.js导入的CLIP_NODE_TYPES,确保UI指示器与实际功能一致 * @param {boolean} enabled */ /* export function updateAutoTranslateIndicators(enabled) { if (!app.canvas?.graph) return; app.canvas.graph._nodes.forEach(node => { // 检查是否为指定的CLIP编码器节点 const isClipNode = node.type && CLIP_NODE_TYPES.includes(node.type); if (isClipNode) { // 查找该节点的小助手实例 const nodeId = node.id; if (node.widgets) { node.widgets.forEach(widget => { const inputId = widget.name || widget.id; if (inputId) { const assistantKey = `${nodeId}_${inputId}`; const assistantInstance = PromptAssistant.getInstance(assistantKey); // 如果找到小助手实例,更新其样式 if (assistantInstance && assistantInstance.element) { assistantInstance.element.classList.toggle('auto-translate-enabled', enabled); } } }); } } }); } */ /** * 暂时禁用的自动翻译指示器功能的空占位函数 * 保持导出以避免其他模块的导入错误 */ export function updateAutoTranslateIndicators(enabled) { // 空实现,不执行任何操作 logger.debug("自动翻译指示器功能已暂时禁用"); return; } // ====================== 工具函数 ====================== /** * 生成唯一请求ID */ function generateRequestId(prefix = 'baidu_trans', type = null) { // 如果提供了type,根据type确定前缀 if (type) { switch (type) { case 'llm': prefix = 'llm_trans'; break; case 'baidu': prefix = 'baidu_trans'; break; case 'expand': prefix = 'glm4_expand'; break; default: // 保持传入的prefix不变 break; } } const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 6); return `${prefix}_${timestamp}_${random}`; } /** * 防抖函数 * 限制函数调用频率,避免频繁触发导致性能问题 */ function debounce(func, wait = 100) { return EventManager.debounce(func, wait); } // ====================== 主类实现 ====================== /** * 提示词小助手主类 * 统一管理小助手的生命周期、实例和资源 */ class PromptAssistant { /** 存储所有小助手实例的Map集合 */ static instances = new Map(); constructor() { this.initialized = false; } // ---生命周期管理功能--- /** * 判断功能是否被禁用 */ areAllFeaturesDisabled() { return !window.FEATURES.enabled; } /** * 初始化提示词小助手 */ initialize() { if (this.initialized) return; try { // 检查版本号 if (!window.PromptAssistant_Version) { logger.error("初始化时未找到版本号!这可能导致UI显示异常"); } else { logger.debug(`初始化时检测到版本号: ${window.PromptAssistant_Version}`); } // 初始化事件管理器 EventManager.init(); // 检查总开关的初始状态 const initialEnabled = app.ui.settings.getSettingValue("PromptAssistant.Features.Enabled"); window.FEATURES.enabled = initialEnabled !== undefined ? initialEnabled : true; // 记录总开关状态(改为调试级别) logger.debug(`初始化时检测总开关状态 | 状态:${window.FEATURES.enabled ? "启用" : "禁用"}`); // 初始化资源管理器 ResourceManager.init(); // 只有在总开关打开时才做完整初始化 if (window.FEATURES.enabled) { // 更新CLIP节点的指示器状态 updateAutoTranslateIndicators(window.FEATURES.autoTranslate); } this.initialized = true; logger.log("初始化完成 | 小助手已完全启动"); } catch (error) { logger.error(`初始化失败 | 错误: ${error.message}`); // 重置状态 this.initialized = false; window.FEATURES.enabled = false; // 确保清理 this.cleanup(); } } /** * 统一控制总开关功能 * 集中管理所有受总开关控制的服务功能 */ async toggleGlobalFeature(enable, force = false) { // 更新状态 const oldValue = window.FEATURES.enabled; window.FEATURES.enabled = enable; // 状态未变化时不执行操作,除非force为true if (!force && oldValue === enable) { return; } // 仅当状态真正变化或强制执行时才记录日志 if (oldValue !== enable || force === true) { logger.log(`总开关 | 动作:${enable ? "启用" : "禁用"}`); } try { if (enable) { // === 启用所有服务 === // 确保管理器已初始化 if (!EventManager.initialized) { EventManager.init(); } if (!ResourceManager.isInitialized()) { ResourceManager.init(); } // 1. 重置节点初始化标记,准备重新检测 if (app.canvas && app.canvas.graph) { const nodes = app.canvas.graph._nodes || []; nodes.forEach(node => { if (node) { node._promptAssistantInitialized = false; } }); } // 2. 设置或恢复节点选择事件监听 if (app.canvas) { // 避免重复设置监听器 if (!app.canvas._promptAssistantSelectionHandler) { app.canvas._promptAssistantSelectionHandler = function (selected_nodes) { // 当总开关关闭时,跳过所有节点处理 if (!window.FEATURES.enabled) { return; } if (selected_nodes && Object.keys(selected_nodes).length > 0) { Object.keys(selected_nodes).forEach(nodeId => { const node = app.canvas.graph.getNodeById(nodeId); if (!node) return; // 初始化未初始化的节点 if (!node._promptAssistantInitialized) { node._promptAssistantInitialized = true; this.checkAndSetupNode(node); } }); } }.bind(this); } // 保存当前监听器并设置新的 if (app.canvas.onSelectionChange && app.canvas.onSelectionChange !== app.canvas._promptAssistantSelectionHandler) { app.canvas._originalSelectionChange = app.canvas.onSelectionChange; } app.canvas.onSelectionChange = app.canvas._promptAssistantSelectionHandler; } } else { // === 禁用所有服务 === // 1. 计数并清理所有实例 const instanceCount = PromptAssistant.instances.size; this.cleanup(null, true); // 2. 恢复原始节点选择事件监听 if (app.canvas) { if (app.canvas._originalSelectionChange) { app.canvas.onSelectionChange = app.canvas._originalSelectionChange; } else { app.canvas.onSelectionChange = null; } } } // 按钮可见性更新在features中单独处理 window.FEATURES.updateButtonsVisibility(); // 如果功能已启用,更新自动翻译指示器状态 /* 暂时禁用自动翻译指示器 if (enable) { updateAutoTranslateIndicators(window.FEATURES.autoTranslate); } */ } catch (error) { logger.error(`总开关操作失败 | 错误: ${error.message}`); // 恢复原始状态 window.FEATURES.enabled = oldValue; } } // ---资源管理功能--- /** * 清理所有资源 */ cleanup(nodeId = null, silent = false) { // 如果正在切换工作流,则只清理UI实例,不删除缓存 if (window.PROMPT_ASSISTANT_WORKFLOW_SWITCHING) { // 简化日志:工作流切换期间不逐条打印节点清理日志,避免高频刷屏 // 如需排查问题,可将下行改回 debug 单条输出 // if (nodeId !== null) { logger.debug(`[清理跳过] 正在切换工作流,仅清理提示词小助手UI,节点ID: ${nodeId}`); } const keysToDelete = Array.from(PromptAssistant.instances.keys()) .filter(key => nodeId === null || key.startsWith(`${String(nodeId)}_`)); keysToDelete.forEach(key => { const instance = PromptAssistant.getInstance(key); if (instance) { this._cleanupInstance(instance, key, false); // false表示从实例集合中移除 } }); // 如果是全局清理,清空实例集合 if (nodeId === null) { PromptAssistant.instances.clear(); } return; } // 检查nodeId是否有效 if (nodeId !== null && nodeId !== undefined) { // 确保nodeId是字符串类型,便于后续比较 const nodeIdStr = String(nodeId); // 获取当前节点的所有实例键 const keysToDelete = Array.from(PromptAssistant.instances.keys()) .filter(key => key === nodeIdStr || key.startsWith(`${nodeIdStr}_`)); // 如果有实例需要清理 if (keysToDelete.length > 0) { let historyCount = 0; let tagCount = 0; let instanceNames = []; try { // 统计并清理历史记录 const allHistory = HistoryCacheService.getAllHistory(); historyCount = allHistory.filter(item => item.node_id === nodeId).length; HistoryCacheService.clearNodeHistory(nodeId); // 统计并清理标签缓存 keysToDelete.forEach(key => { const instance = PromptAssistant.getInstance(key); if (instance && instance.inputId) { const tags = TagCacheService.getAllRawTags(nodeId, instance.inputId); tagCount += tags ? tags.length : 0; TagCacheService.clearCache(nodeId, instance.inputId); instanceNames.push(instance.inputId); } }); // 清理实例 keysToDelete.forEach(key => { const instance = PromptAssistant.getInstance(key); if (instance) { this._cleanupInstance(instance, key, true); PromptAssistant.instances.delete(key); } }); if (!silent) { // 获取当前剩余的统计信息 const remainingInstances = PromptAssistant.instances.size; // 获取标签缓存统计 const tagStats = TagCacheService.getTagStats(); const remainingTags = tagStats.total; const remainingHistory = HistoryCacheService.getAllHistory().length; logger.log(`[节点清理] 节点ID: ${nodeId} | 实例: ${instanceNames.join(', ')} | 历史: ${historyCount}条 | 标签: ${tagCount}个`); logger.log(`[剩余统计] 小助手实例: ${remainingInstances}个 | 标签缓存: ${remainingTags}个 | 节点历史缓存: ${remainingHistory}条`); } } catch (error) { logger.error(`[节点清理] 失败 | 节点ID: ${nodeId} | 错误: ${error.message}`); } } return; } // 清理所有实例和历史 const beforeCleanupSize = PromptAssistant.instances.size; if (beforeCleanupSize > 0) { let totalHistoryCount = 0; let totalTagCount = 0; let allInstanceNames = []; try { // 统计并清理所有历史记录 const allHistory = HistoryCacheService.getAllHistory(); totalHistoryCount = allHistory.length; HistoryCacheService.clearAllHistory(); // 统计标签缓存 const tagStats = TagCacheService.getTagStats(); totalTagCount = tagStats.total; // 清理所有标签缓存 TagCacheService.clearAllTagCache(); // 清理所有实例 for (const [key, instance] of PromptAssistant.instances) { if (instance) { allInstanceNames.push(instance.inputId || key); this._cleanupInstance(instance, key, true); } } // 清空实例集合 PromptAssistant.instances.clear(); if (!silent) { logger.log(`[全局清理] 实例: ${allInstanceNames.join(', ')} | 历史: ${totalHistoryCount}条 | 标签: ${totalTagCount}个`); logger.log(`[剩余统计] 小助手实例: 0个 | 标签缓存: 0个 | 节点历史缓存: 0条`); } } catch (error) { logger.error(`[全局清理] 失败 | 错误: ${error.message}`); } } } // ---实例管理功能--- /** * 检查节点是否有效 */ static isValidNode(node) { return node && typeof node.id !== 'undefined' && node.id !== -1 && // 防止id为-1的无效节点 node.widgets && // 确保有widgets属性 typeof node.type === 'string'; // 确保有类型 } /** * 添加实例到管理器 */ static addInstance(nodeId, widget) { if (nodeId != null && widget != null) { this.instances.set(String(nodeId), widget); return true; } return false; } /** * 获取实例 */ static getInstance(nodeId) { if (nodeId == null) return null; return this.instances.get(String(nodeId)); } /** * 检查实例是否存在 */ static hasInstance(nodeId) { if (nodeId == null) return false; return this.instances.has(String(nodeId)); } /** * 检查节点并设置小助手 * 查找节点中的有效输入控件并创建小助手 */ checkAndSetupNode(node) { // 始终首先检查总开关状态 if (!window.FEATURES.enabled || !node || !node.widgets) { return; } // 获取所有有效的输入控件 const validInputs = node.widgets.filter(widget => { return UIToolkit.isValidInput(widget); }); if (validInputs.length === 0) { return; } // 为每个有效控件创建小助手 validInputs.forEach(inputWidget => { const inputId = inputWidget.name || inputWidget.id; const assistantKey = `${node.id}_${inputId}`; // 检查实例是否已存在 if (PromptAssistant.hasInstance(assistantKey)) { // 如果实例存在,检查输入控件是否已更新 const instance = PromptAssistant.getInstance(assistantKey); if (instance && instance.text_element !== inputWidget.inputEl) { // 如果输入控件已更新,清理旧实例并创建新实例 this.cleanup(node.id); } else { return; // 跳过已存在且未更新的实例 } } // 再次检查总开关状态,确保在创建过程中没有被禁用 if (!window.FEATURES.enabled) { return; } // 创建小助手实例 const assistant = this.setupNodeAssistant(node, inputWidget); if (assistant) { logger.log(`创建小助手 | 节点:${node.id} | 控件:${inputId} | 实例:${assistantKey}`); // 检查是否为指定的CLIP编码器节点,并应用自动翻译样式 // 使用从interceptor.js导入的CLIP_NODE_TYPES,确保UI指示器与实际功能一致 // 暂时禁用自动翻译检测 /* const isClipNode = node.type && CLIP_NODE_TYPES.includes(node.type); if (isClipNode && assistant.element) { assistant.element.classList.toggle('auto-translate-enabled', window.FEATURES.autoTranslate); } */ } }); } /** * 为节点设置小助手 * 创建小助手实例并初始化显示状态 */ setupNodeAssistant(node, inputWidget) { // 简化参数检查 if (!node || !inputWidget) { return null; } try { const nodeId = node.id; const inputId = inputWidget.name || inputWidget.id || Math.random().toString(36).substring(2, 10); const isNoteNode = node.type === "Note"; // 简化节点信息 const nodeInfo = { workflow_id: app.graph?._workflow_id || 'unknown', nodeType: node.type, inputType: inputId, isNoteNode: isNoteNode }; // 创建小助手实例 const assistant = this.createAssistant( node, inputId, isNoteNode ? { ...inputWidget, inputEl: inputWidget.element } : inputWidget, // Note节点特殊处理 nodeInfo ); if (assistant) { // 初始化显示状态,始终显示 this.showAssistantUI(assistant); logger.log("小助手创建并显示 | 节点ID:" + nodeId); return assistant; } return null; } catch (error) { logger.error(`创建小助手 | 结果:异常 | 节点ID: ${node.id}, 错误: ${error.message}`); return null; } } /** * 创建小助手实例 * 根据节点和输入控件构建小助手对象并初始化UI */ createAssistant(node, inputId, inputWidget, nodeInfo = {}) { // 简化前置检查 if (!window.FEATURES.enabled || !node || !inputId || !inputWidget || !UIToolkit.isValidInput(inputWidget)) { return null; } // 确保输入元素存在 const inputEl = inputWidget.inputEl || inputWidget.element; if (!inputEl) { return null; } const nodeId = node.id; const widgetKey = `${nodeId}_${inputId}`; // 检查是否已存在实例 if (PromptAssistant.hasInstance(widgetKey)) { return PromptAssistant.getInstance(widgetKey); } // 创建小助手对象 const widget = { type: "prompt_assistant", name: inputId, nodeId, inputId, widgetKey, buttons: {}, text_element: inputEl, inputEl: inputEl, // 直接添加inputEl引用,方便历史记录回写 // 不再直接设置 node 引用,而是保存 nodeId nodeInfo: { ...nodeInfo, nodeId: node.id, nodeType: node.type }, isTransitioning: false // 添加状态标记,避免频繁切换 }; // 创建全局输入框映射(如果不存在) if (!window.PromptAssistantInputWidgetMap) { window.PromptAssistantInputWidgetMap = {}; } // 将当前输入框添加到映射 window.PromptAssistantInputWidgetMap[widgetKey] = { inputEl: inputEl, widget: widget }; logger.debug(`输入框映射 | 添加映射 | 键:${widgetKey}`); // 创建UI并添加到实例集合 this.createAssistantUI(widget, inputWidget); PromptAssistant.addInstance(widgetKey, widget); // 初始化撤销状态 HistoryCacheService.initUndoState(nodeId, inputId, inputEl.value); // 初始化时立即更新撤销/重做按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); // 绑定输入框失焦事件,写入历史 if (inputWidget.inputEl) { // 使用事件管理器添加DOM事件监听 const removeBlurListener = EventManager.addDOMListener(inputWidget.inputEl, 'blur', async () => { logger.debug(`历史写入准备| 原因:失焦事件触发 node_id=${node.id} input_id=${inputId}`); HistoryCacheService.addHistory({ workflow_id: nodeInfo?.workflow_id || '', node_id: node.id, input_id: inputId, content: inputWidget.inputEl.value, operation_type: 'input', timestamp: Date.now() }); // 重置撤销状态 HistoryCacheService.initUndoState(node.id, inputId, inputWidget.inputEl.value); // 更新按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); logger.debug(`历史写入完成|原因:输入框失焦 node_id=${node.id} input_id=${inputId}`); }); // 保存清理函数引用,以便后续清理 widget._eventCleanupFunctions = widget._eventCleanupFunctions || []; widget._eventCleanupFunctions.push(removeBlurListener); // 添加输入事件监听,实时更新撤销/重做按钮状态和位置调整 const removeInputListener = EventManager.addDOMListener(inputWidget.inputEl, 'input', () => { UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); // 检测滚动条状态并调整位置 this._adjustPositionForScrollbar(widget, inputWidget.inputEl); }); widget._eventCleanupFunctions.push(removeInputListener); // 添加ResizeObserver监听输入框尺寸变化 if (window.ResizeObserver) { const resizeObserver = new ResizeObserver(() => { // 延迟执行,确保浏览器完成布局更新 setTimeout(() => { this._adjustPositionForScrollbar(widget, inputWidget.inputEl); }, 10); }); resizeObserver.observe(inputWidget.inputEl); // 添加清理函数 widget._eventCleanupFunctions.push(() => { resizeObserver.disconnect(); }); } else { // 降级方案:监听window resize事件 const removeResizeListener = EventManager.addDOMListener(window, 'resize', EventManager.debounce(() => { this._adjustPositionForScrollbar(widget, inputWidget.inputEl); }, 100) ); widget._eventCleanupFunctions.push(removeResizeListener); } } return widget; } // ---UI管理功能--- /** * 创建小助手UI * 构建DOM元素并设置事件监听和初始样式 */ createAssistantUI(widget, inputWidget) { const nodeId = widget.nodeId; const inputId = widget.inputId; try { // 获取DOM相关信息 const inputEl = inputWidget.inputEl; const graphCanvasContainer = document.querySelector('.graphcanvas'); const canvasContainerRect = graphCanvasContainer?.getBoundingClientRect(); // 创建内部内容容器(分离视觉效果) const innerContentDiv = document.createElement('div'); innerContentDiv.className = 'prompt-assistant-inner prompt-assistant-content-fixed'; // 创建主容器 - 简化样式,减少触发栅格化的属性 const containerDiv = document.createElement('div'); containerDiv.className = 'prompt-assistant-container prompt-assistant-transition'; containerDiv.dataset.nodeId = nodeId; containerDiv.dataset.inputId = inputId; // 添加内容容器到主容器 containerDiv.appendChild(innerContentDiv); // 创建悬停区域 - 用于检测鼠标悬停 const hoverAreaDiv = document.createElement('div'); hoverAreaDiv.className = 'prompt-assistant-hover-area'; // 将悬停区域添加到容器中 containerDiv.appendChild(hoverAreaDiv); // 创建折叠状态指示器图标 const indicatorDiv = document.createElement('div'); indicatorDiv.className = 'prompt-assistant-indicator'; // 从ResourceManager获取图标并添加到指示器 const mainIcon = ResourceManager.getIcon('icon-main.svg'); if (mainIcon) { indicatorDiv.appendChild(mainIcon); } // 将指示器添加到容器中 containerDiv.appendChild(indicatorDiv); // 保存引用并初始化 widget.element = containerDiv; widget.innerContent = innerContentDiv; widget.hoverArea = hoverAreaDiv; widget.indicator = indicatorDiv; widget.buttons = {}; widget.isCollapsed = false; // 初始状态为展开 // 初始化UI组件和事件 this.addFunctionButtons(widget); this._setupUIEventHandling(widget, inputEl, containerDiv); this._setupUIPosition(widget, inputEl, containerDiv, canvasContainerRect); // 初始滚动条检测和位置调整 setTimeout(() => { this._adjustPositionForScrollbar(widget, inputEl); }, 100); // 延迟执行,确保DOM完全渲染 // 默认隐藏状态 containerDiv.style.display = 'none'; logger.log(`UI创建 | 结果:完成 | 节点ID: ${nodeId}`); return containerDiv; } catch (error) { logger.error(`UI创建 | 结果:异常 | 节点ID: ${nodeId}, 错误: ${error.message}`); return null; } } /** * 显示小助手UI * 控制UI显示动画和状态,始终保持显示 */ showAssistantUI(widget, forceAnimation = false) { if (!widget?.element) return; // 避免重复显示和不必要的动画 const isCurrentlyShown = widget.element.classList.contains('assistant-show'); if (isCurrentlyShown && !forceAnimation) { // 如果已经显示且不需要强制动画,则仅确保显示状态 widget.element.style.display = 'flex'; widget.element.style.opacity = '1'; return; } // 设置过渡状态 widget.isTransitioning = true; // 优化渲染性能 widget.element.style.willChange = 'auto'; widget.element.style.transform = 'translateZ(0)'; widget.element.style.opacity = '1'; // 显示元素并应用动画类 widget.element.style.display = 'flex'; void widget.element.offsetWidth; // 触发回流,确保动画生效 widget.element.classList.add('assistant-show'); // 动画结束后重置过渡状态 setTimeout(() => { widget.isTransitioning = false; // 检查是否需要自动折叠 this.triggerAutoCollapse(widget); }, 300); // 与CSS动画时长匹配 } /** * 检查并触发自动折叠(如果需要) */ _triggerAutoCollapseIfNeeded(widget) { // 如果widget没有初始化或者已经处于折叠状态,则不处理 if (!widget || !widget.element || widget.isCollapsed || widget.isTransitioning) return; // 如果有活跃按钮,不自动折叠 if (this._checkAssistantActiveState(widget)) return; // 如果鼠标当前悬停在容器上,不自动折叠 if (widget.isMouseOver) return; // 设置自动折叠定时器 if (widget._autoCollapseTimer) { clearTimeout(widget._autoCollapseTimer); } widget._autoCollapseTimer = setTimeout(() => { // 再次检查条件 if (!widget.isCollapsed && !widget.isTransitioning && !this._checkAssistantActiveState(widget) && !widget.isMouseOver) { const containerDiv = widget.element; // 保存当前宽度用于展开动画 if (containerDiv.offsetWidth > 0) { containerDiv.style.setProperty('--expanded-width', `${containerDiv.offsetWidth}px`); } // 设置过渡状态 widget.isTransitioning = true; // 直接添加折叠类 containerDiv.classList.add('collapsed'); widget.isCollapsed = true; // 显示悬停区域,用于检测鼠标悬停以展开UI if (widget.hoverArea) { widget.hoverArea.style.display = 'block'; } // 动画结束后重置过渡状态 setTimeout(() => { widget.isTransitioning = false; }, 300); } widget._autoCollapseTimer = null; }, 1000); // 确保清理函数中包含这个定时器 if (widget._eventCleanupFunctions) { widget._eventCleanupFunctions.push(() => { if (widget._autoCollapseTimer) { clearTimeout(widget._autoCollapseTimer); widget._autoCollapseTimer = null; } }); } } /** * 展开小助手 */ _expandAssistant(widget) { if (!widget || !widget.element || !widget.isCollapsed || widget.isTransitioning) return; widget.isTransitioning = true; // 隐藏悬停区域,避免覆盖按钮 if (widget.hoverArea) { widget.hoverArea.style.display = 'none'; } const containerDiv = widget.element; // 计算展开后的宽度 let targetWidth = containerDiv.style.getPropertyValue('--expanded-width'); // 如果没有保存宽度或需要重新计算 if (!targetWidth || targetWidth === '') { // 先移除折叠类,但保持不可见状态进行测量 containerDiv.classList.remove('collapsed'); const originalDisplay = containerDiv.style.display; const originalOpacity = containerDiv.style.opacity; // 临时设置样式以便测量 containerDiv.style.opacity = '0'; containerDiv.style.display = 'flex'; containerDiv.style.position = 'absolute'; // 强制回流并测量 void containerDiv.offsetWidth; // 获取自然宽度 const naturalWidth = containerDiv.offsetWidth; targetWidth = naturalWidth + 'px'; // 保存宽度供将来使用 containerDiv.style.setProperty('--expanded-width', targetWidth); // 恢复原始样式 containerDiv.style.opacity = originalOpacity; containerDiv.style.position = ''; // 重新应用折叠类以准备动画 containerDiv.classList.add('collapsed'); void containerDiv.offsetWidth; // 强制回流 } // 手动设置宽度转换 containerDiv.style.width = '28px'; // 起始宽度 // 强制回流 void containerDiv.offsetWidth; // 移除折叠类 containerDiv.classList.remove('collapsed'); // 设置目标宽度以触发过渡 containerDiv.style.width = targetWidth; // 动画结束后清理 setTimeout(() => { // 移除固定宽度,恢复自动宽度 containerDiv.style.width = ''; widget.isCollapsed = false; widget.isTransitioning = false; }, 300); } /** * 公开方法:触发小助手自动折叠 * 供外部模块调用,用于在操作完成后折叠小助手UI */ triggerAutoCollapse(widget) { return this._triggerAutoCollapseIfNeeded(widget); } /** * 更新小助手可见性 * 始终显示小助手,不再根据鼠标悬停状态来决定 */ updateAssistantVisibility(widget) { if (!widget) return; // 总开关关闭时不处理可见性更新 if (!window.FEATURES || !window.FEATURES.enabled) { return; } // 检查是否有按钮处于激活或处理中状态 const hasActiveButtons = this._checkAssistantActiveState(widget); // 如果有激活的按钮,强制显示小助手(带动画)并取消自动折叠 if (hasActiveButtons) { this.showAssistantUI(widget, true); // 取消可能的自动折叠定时器 if (widget._autoCollapseTimer) { clearTimeout(widget._autoCollapseTimer); widget._autoCollapseTimer = null; } // 如果当前是折叠状态,则展开 if (widget.isCollapsed) { this._expandAssistant(widget); } return; } // 始终显示小助手,不再检查鼠标状态 const isCurrentlyShown = widget.element?.classList.contains('assistant-show'); if (!isCurrentlyShown) { this.showAssistantUI(widget, false); logger.debug(`UI显示 | 节点:${widget.nodeId} | 原因:始终显示`); } else { // 已经显示,检查是否需要自动折叠 this.triggerAutoCollapse(widget); } } /** * 检查小助手是否有按钮处于激活状态 */ _checkAssistantActiveState(widget) { if (!widget || !widget.buttons) return false; // 首先检查中央按钮状态管理器是否有该widget的激活按钮 const activeButtonInfo = UIToolkit.getActiveButtonInfo(); if (activeButtonInfo && activeButtonInfo.widget === widget) { return true; } // 再遍历所有按钮,检查是否有按钮处于active或processing状态 for (const buttonId in widget.buttons) { const button = widget.buttons[buttonId]; if (button.classList.contains('button-active') || button.classList.contains('button-processing')) { return true; } } return false; } /** * 更新所有实例的可见性 * 在按钮状态变化时调用 */ updateAllInstancesVisibility() { PromptAssistant.instances.forEach(widget => { this.updateAssistantVisibility(widget); }); } /** * 显示状态提示 * 创建临时提示信息气泡 */ showStatusTip(anchorElement, type, message, position = null) { return UIToolkit.showStatusTip(anchorElement, type, message, position); } // ---事件处理功能--- /** * 设置UI事件处理 * 配置按钮事件监听 - 简化版本 */ _setupUIEventHandling(widget, inputEl, containerDiv) { // 设置容器可以接收事件,而不是穿透 containerDiv.style.pointerEvents = 'auto'; widget.innerContent.style.pointerEvents = 'auto'; // 确保内容容器可以接收事件 widget.hoverArea.style.pointerEvents = 'auto'; // 确保悬停区域可以接收事件 widget.hoverArea.style.display = 'none'; // 初始状态下隐藏悬停区域,避免覆盖按钮 // 确保所有按钮可点击 const buttons = containerDiv.querySelectorAll('button'); buttons.forEach(button => { // button.style.pointerEvents = 'auto'; // 移除此行,由CSS控制指针事件 button.classList.add('prompt-assistant-button-active'); }); // 初始化过渡状态标记 widget.isTransitioning = false; // 使用清理函数数组存储所有事件清理函数 widget._eventCleanupFunctions = widget._eventCleanupFunctions || []; // 折叠和展开功能 const setupCollapseExpandEvents = () => { // 记录原始宽度,用于展开动画 const saveOriginalWidth = () => { if (!widget.isCollapsed && containerDiv.offsetWidth > 0) { containerDiv.style.setProperty('--expanded-width', `${containerDiv.offsetWidth}px`); } }; // 延迟保存宽度,确保DOM已完全渲染 setTimeout(saveOriginalWidth, 300); // 折叠函数 const collapseAssistant = () => { if (widget.isCollapsed || widget.isTransitioning) return; // 保存当前宽度用于展开动画 saveOriginalWidth(); widget.isTransitioning = true; // 直接添加折叠类,不使用动画类 containerDiv.classList.add('collapsed'); widget.isCollapsed = true; // 显示悬停区域,用于检测鼠标悬停以展开UI widget.hoverArea.style.display = 'block'; // 动画结束后重置过渡状态 setTimeout(() => { widget.isTransitioning = false; }, 300); }; // 展开函数 const expandAssistant = () => { if (!widget.isCollapsed || widget.isTransitioning) return; widget.isTransitioning = true; // 隐藏悬停区域,避免覆盖按钮 widget.hoverArea.style.display = 'none'; // 计算展开后的宽度 let targetWidth = widget.element.style.getPropertyValue('--expanded-width'); // 如果没有保存宽度或需要重新计算 if (!targetWidth || targetWidth === '') { // 先移除折叠类,但保持不可见状态进行测量 containerDiv.classList.remove('collapsed'); const originalDisplay = containerDiv.style.display; const originalOpacity = containerDiv.style.opacity; // 临时设置样式以便测量 containerDiv.style.opacity = '0'; containerDiv.style.display = 'flex'; containerDiv.style.position = 'absolute'; // 强制回流并测量 void containerDiv.offsetWidth; // 获取自然宽度 const naturalWidth = containerDiv.offsetWidth; targetWidth = naturalWidth + 'px'; // 保存宽度供将来使用 containerDiv.style.setProperty('--expanded-width', targetWidth); // 恢复原始样式 containerDiv.style.opacity = originalOpacity; containerDiv.style.position = ''; // 重新应用折叠类以准备动画 containerDiv.classList.add('collapsed'); void containerDiv.offsetWidth; // 强制回流 } // 手动设置宽度转换 containerDiv.style.width = '28px'; // 起始宽度 // 强制回流 void containerDiv.offsetWidth; // 移除折叠类 containerDiv.classList.remove('collapsed'); // 设置目标宽度以触发过渡 containerDiv.style.width = targetWidth; // 动画结束后清理 setTimeout(() => { // 移除固定宽度,恢复自动宽度 containerDiv.style.width = ''; widget.isCollapsed = false; widget.isTransitioning = false; }, 300); }; // 创建折叠定时器变量 let collapseTimer = null; let autoCollapseTimer = null; // 鼠标离开容器时折叠 const handleMouseLeave = () => { // 如果有活跃按钮,不折叠 if (this._checkAssistantActiveState(widget)) return; // 设置延时,避免鼠标短暂离开就触发折叠 collapseTimer = setTimeout(() => { collapseAssistant(); }, 500); }; // 鼠标进入容器(展开时)以保持展开状态 const handleContainerMouseEnter = () => { // 仅当小助手展开时,进入容器才会阻止其自动折叠 if (widget.isCollapsed) return; if (collapseTimer) { clearTimeout(collapseTimer); collapseTimer = null; } if (autoCollapseTimer) { clearTimeout(autoCollapseTimer); autoCollapseTimer = null; } }; // 鼠标进入悬停区域(折叠时)以展开 const handleHoverAreaMouseEnter = () => { if (collapseTimer) { clearTimeout(collapseTimer); collapseTimer = null; } if (autoCollapseTimer) { clearTimeout(autoCollapseTimer); autoCollapseTimer = null; } // 如果当前是折叠状态,则展开 if (widget.isCollapsed) { expandAssistant(); } }; // 为容器添加鼠标事件 const removeContainerMouseLeave = EventManager.addDOMListener(containerDiv, 'mouseleave', handleMouseLeave); const removeContainerMouseEnter = EventManager.addDOMListener(containerDiv, 'mouseenter', handleContainerMouseEnter); // 为悬停区域添加鼠标事件 const removeHoverAreaMouseEnter = EventManager.addDOMListener(widget.hoverArea, 'mouseenter', handleHoverAreaMouseEnter); // 添加清理函数 widget._eventCleanupFunctions.push(removeContainerMouseLeave); widget._eventCleanupFunctions.push(removeContainerMouseEnter); widget._eventCleanupFunctions.push(removeHoverAreaMouseEnter); // 添加清理定时器的函数 widget._eventCleanupFunctions.push(() => { if (collapseTimer) { clearTimeout(collapseTimer); collapseTimer = null; } if (autoCollapseTimer) { clearTimeout(autoCollapseTimer); autoCollapseTimer = null; } }); // 创建后自动折叠功能 const setupAutoCollapse = () => { // 如果有活跃按钮,不自动折叠 if (this._checkAssistantActiveState(widget)) return; // 设置自动折叠定时器,1秒后自动折叠 autoCollapseTimer = setTimeout(() => { // 再次检查是否有活跃按钮或鼠标悬停在容器上 if (!this._checkAssistantActiveState(widget) && !widget.isMouseOver) { collapseAssistant(); } }, 1000); }; // 添加鼠标悬停状态跟踪 widget.isMouseOver = false; const trackMouseOver = () => { widget.isMouseOver = true; }; const trackMouseOut = () => { widget.isMouseOver = false; }; // 为容器和悬停区域添加鼠标悬停状态跟踪 const removeContainerMouseOverTracking = EventManager.addDOMListener(containerDiv, 'mouseover', trackMouseOver); const removeContainerMouseOutTracking = EventManager.addDOMListener(containerDiv, 'mouseout', trackMouseOut); const removeHoverAreaMouseOverTracking = EventManager.addDOMListener(widget.hoverArea, 'mouseover', trackMouseOver); const removeHoverAreaMouseOutTracking = EventManager.addDOMListener(widget.hoverArea, 'mouseout', trackMouseOut); // 添加清理函数 widget._eventCleanupFunctions.push(removeContainerMouseOverTracking); widget._eventCleanupFunctions.push(removeContainerMouseOutTracking); widget._eventCleanupFunctions.push(removeHoverAreaMouseOverTracking); widget._eventCleanupFunctions.push(removeHoverAreaMouseOutTracking); // 设置自动折叠(延迟执行,确保DOM已完全渲染) setTimeout(setupAutoCollapse, 500); }; // 设置折叠展开事件 setupCollapseExpandEvents(); // 添加清理函数 widget.cleanupListeners = () => { // 执行所有保存的清理函数 if (widget._eventCleanupFunctions && widget._eventCleanupFunctions.length) { widget._eventCleanupFunctions.forEach(cleanupFn => { if (typeof cleanupFn === 'function') { cleanupFn(); } }); widget._eventCleanupFunctions = []; } }; } // ---辅助功能--- /** * 更新输入框内容并添加高亮效果 */ updateInputWithHighlight(widget, content) { if (!widget?.inputEl) return; try { // 更新输入框内容 widget.inputEl.value = content; // 触发输入事件 widget.inputEl.dispatchEvent(new Event('input', { bubbles: true })); widget.inputEl.dispatchEvent(new Event('change', { bubbles: true })); // 添加高亮效果 widget.inputEl.classList.add('input-highlight'); // 动画结束后移除类 setTimeout(() => { widget.inputEl.classList.remove('input-highlight'); }, 200); // 与 CSS 动画时长匹配 logger.debug(`输入框更新 | 结果:成功 | 内容长度:${content.length}`); } catch (error) { logger.error(`输入框更新 | 结果:异常 | 错误:${error.message}`); } } // ---按钮管理功能--- /** * 添加功能按钮 */ addFunctionButtons(widget) { if (!widget?.element) { logger.error('添加按钮 | 结果:失败 | 原因: 容器不存在'); return; } // 检查总开关状态 if (!FEATURES.enabled) { logger.debug('添加按钮 | 结果:跳过 | 原因: 总功能已禁用'); return; } // 检查是否有至少一个功能启用 const hasEnabledFeatures = FEATURES.history || FEATURES.tag || FEATURES.expand || FEATURES.translate; if (!hasEnabledFeatures) { logger.debug('添加按钮 | 结果:跳过 | 原因: 没有启用任何功能'); return; } // 检查是否是Note节点 const isNoteNode = widget.nodeInfo && widget.nodeInfo.isNoteNode === true; // 获取历史状态(用于初始化撤销/重做按钮状态) const canUndo = HistoryCacheService.canUndo(widget.nodeId, widget.inputId); const canRedo = HistoryCacheService.canRedo(widget.nodeId, widget.inputId); // 按钮配置 const buttonConfigs = [ { id: 'history', title: '历史', icon: 'icon-history', onClick: (e, widget) => { UIToolkit.handlePopupButtonClick( e, widget, 'history', HistoryManager.showHistoryPopup.bind(HistoryManager), HistoryManager.hideHistoryPopup.bind(HistoryManager) ); }, visible: !isNoteNode && FEATURES.history, // Note节点不显示此按钮 initialState: { disabled: false } }, { id: 'undo', title: '撤销', icon: 'icon-undo', onClick: (e, widget) => { e.preventDefault(); e.stopPropagation(); logger.debug('按钮点击 | 动作: 撤销'); // 执行撤销操作 const undoContent = HistoryCacheService.undo(widget.nodeId, widget.inputId); if (undoContent !== null) { // 更新输入框内容并添加高亮效果 this.updateInputWithHighlight(widget, undoContent); // 更新按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); logger.debug(`撤销操作 | 结果:成功 | 节点:${widget.nodeId}`); } else { logger.debug(`撤销操作 | 结果:失败 | 节点:${widget.nodeId} | 原因:无可用内容`); } }, visible: !isNoteNode && FEATURES.history, initialState: { disabled: !canUndo } }, { id: 'redo', title: '重做', icon: 'icon-redo', onClick: (e, widget) => { e.preventDefault(); e.stopPropagation(); logger.debug('按钮点击 | 动作: 重做'); // 执行重做操作 const redoContent = HistoryCacheService.redo(widget.nodeId, widget.inputId); if (redoContent !== null) { // 更新输入框内容并添加高亮效果 this.updateInputWithHighlight(widget, redoContent); // 更新按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); logger.debug(`重做操作 | 结果:成功 | 节点:${widget.nodeId}`); } else { logger.debug(`重做操作 | 结果:失败 | 节点:${widget.nodeId} | 原因:无可用内容`); } }, visible: !isNoteNode && FEATURES.history, initialState: { disabled: !canRedo } }, { id: 'divider1', type: 'divider', visible: !isNoteNode // Note节点不显示分割线 }, { id: 'tag', title: '标签工具', icon: 'icon-tag', onClick: (e, widget) => { // 创建一个带有标签选择功能的显示函数 const showTagPopup = (options) => { // 处理标签选择功能 const enhancedOptions = { ...options, onTagSelect: (tag) => { // 获取当前输入框的值和光标位置 const currentValue = widget.inputEl.value; const cursorPos = widget.inputEl.selectionStart; const beforeText = currentValue.substring(0, cursorPos); const afterText = currentValue.substring(widget.inputEl.selectionEnd); // 添加标签(英文值) const newValue = beforeText + tag.en + afterText; // 更新输入框内容并添加高亮效果 this.updateInputWithHighlight(widget, newValue); // 更新光标位置 const newPos = cursorPos + tag.en.length; widget.inputEl.setSelectionRange(newPos, newPos); // 保持焦点在输入框 widget.inputEl.focus(); } }; // 调用标签管理器显示弹窗 TagManager.showTagPopup(enhancedOptions); }; // 使用统一的弹窗按钮点击处理 UIToolkit.handlePopupButtonClick( e, widget, 'tag', showTagPopup, TagManager.hideTagPopup.bind(TagManager) ); }, visible: !isNoteNode && FEATURES.tag // Note节点不显示此按钮 }, { id: 'expand', title: '扩写', icon: 'icon-expand', onClick: async (e, widget) => { logger.debug('按钮点击 | 动作: 扩写'); // 如果按钮处于 processing 状态且被点击,直接返回, // 让UIToolkit中的取消逻辑接管 if (e.currentTarget.classList.contains('button-processing')) { return; } await UIToolkit.handleAsyncButtonOperation( widget, 'expand', e.currentTarget, async (notifyCancelReady) => { try { // 获取输入值 const inputValue = widget.inputEl.value; if (!inputValue || inputValue.trim() === '') { throw new Error('请输入要扩写的内容'); } // 生成唯一request_id const request_id = generateRequestId('glm4_expand'); // 通知UI可以准备取消操作了 notifyCancelReady(request_id); // 显示扩写中提示 const btnRect = e.currentTarget.getBoundingClientRect(); UIToolkit.showStatusTip( e.currentTarget, 'loading', '扩写中', { x: btnRect.left + btnRect.width / 2, y: btnRect.top } ); // 调用LLM服务进行扩写 const result = await APIService.llmExpandPrompt(inputValue, request_id); if (result.success) { // 更新输入框内容并添加高亮效果 this.updateInputWithHighlight(widget, result.data.expanded); // 添加扩写结果到历史记录 HistoryCacheService.addHistory({ workflow_id: widget.nodeInfo?.workflow_id || '', node_id: widget.nodeId, input_id: widget.inputId, content: result.data.expanded, operation_type: 'expand', request_id: request_id, timestamp: Date.now() }); // 重置撤销状态 HistoryCacheService.initUndoState(widget.nodeId, widget.inputId, result.data.expanded); // 更新按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); return { success: true, useCache: false, tipType: 'success', tipMessage: '扩写完成' }; } else { // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理 throw new Error(result.error); } } catch (error) { // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理 throw error; } } ); }, visible: !isNoteNode && FEATURES.expand, // Note节点不显示此按钮 // 添加右键菜单配置 contextMenu: async (widget) => { // 获取当前激活的扩写提示词ID let activePromptId = null; let expandPrompts = []; try { // 从服务器获取系统提示词配置 const response = await fetch('/prompt_assistant/api/config/system_prompts'); if (response.ok) { const data = await response.json(); // 获取激活的提示词ID activePromptId = data.active_prompts?.expand || null; // 转换扩写规则数据 if (data.expand_prompts) { // 保存原始顺序 const originalOrder = Object.keys(data.expand_prompts); // 使用原始顺序遍历键 originalOrder.forEach(key => { const prompt = data.expand_prompts[key]; expandPrompts.push({ id: key, name: prompt.name || key, content: prompt.content, isActive: key === activePromptId }); }); // 按原始顺序排序 expandPrompts.sort((a, b) => originalOrder.indexOf(a.id) - originalOrder.indexOf(b.id) ); } } } catch (error) { logger.error(`获取扩写提示词失败: ${error.message}`); } // 如果没有获取到提示词,显示一个提示 if (expandPrompts.length === 0) { return [ { label: '未找到扩写提示词', disabled: true } ]; } // 创建菜单项 const menuItems = expandPrompts.map(prompt => ({ label: prompt.name, icon: ``, onClick: async (context) => { logger.log(`右键菜单 | 动作: 切换扩写提示词 | ID: ${prompt.id}`); try { // 更新激活的提示词 const response = await fetch('/prompt_assistant/api/config/active_prompt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'expand', prompt_id: prompt.id }) }); if (response.ok) { // 显示成功提示 UIToolkit.showStatusTip( context.widget.buttons.expand, 'success', `已切换到: ${prompt.name}`, null ); } else { throw new Error(`服务器返回错误: ${response.status}`); } } catch (error) { logger.error(`切换扩写提示词失败: ${error.message}`); UIToolkit.showStatusTip( context.widget.buttons.expand, 'error', `切换失败: ${error.message}`, null ); } } })); menuItems.push({ type: 'separator' }); menuItems.push({ label: '规则管理', icon: '', onClick: () => { rulesConfigManager.showRulesConfigModal(); } }); // 在规则管理下方添加 服务选择 子菜单(LLM提供商) try { const resp = await fetch('/prompt_assistant/api/config/llm'); if (resp.ok) { const cfg = await resp.json(); const providers = cfg.providers || {}; const current = cfg.provider || null; const providerNameMap = { zhipu: '智谱', siliconflow: '硅基流动', "302ai": '302.AI', ollama: 'Ollama', custom: '自定义' }; const order = ['zhipu', 'siliconflow', '302ai', 'ollama', 'custom']; const children = order .filter(key => Object.prototype.hasOwnProperty.call(providers, key)) .map(key => ({ label: providerNameMap[key] || key, icon: ``, onClick: async (context) => { try { const res = await fetch('/prompt_assistant/api/config/llm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_provider: key }) }); if (!res.ok) throw new Error(`服务器返回错误: ${res.status}`); UIToolkit.showStatusTip(context.buttonElement, 'success', `已切换到: ${providerNameMap[key] || key}`); } catch (err) { logger.error(`切换LLM提供商失败: ${err.message}`); UIToolkit.showStatusTip(context.buttonElement, 'error', `切换失败: ${err.message}`); } } })); menuItems.push({ label: '服务选择', icon: '', children }); } } catch (e) { logger.error(`加载LLM提供商失败: ${e.message}`); } return menuItems; } }, { id: 'translate', title: '翻译', icon: 'icon-translate', onClick: async (e, widget) => { logger.debug('按钮点击 | 动作: 翻译'); // 如果按钮处于 processing 状态且被点击,直接返回, // 让UIToolkit中的取消逻辑接管 if (e.currentTarget.classList.contains('button-processing')) { return; } await UIToolkit.handleAsyncButtonOperation( widget, 'translate', e.currentTarget, async (notifyCancelReady) => { try { // 获取输入值 const inputValue = widget.inputEl.value; if (!inputValue || inputValue.trim() === '') { throw new Error('请输入要翻译的内容'); } // 显示翻译中提示 const btnRect = e.currentTarget.getBoundingClientRect(); UIToolkit.showStatusTip( e.currentTarget, 'loading', '翻译中', { x: btnRect.left + btnRect.width / 2, y: btnRect.top } ); // 1. 查询缓存 let cacheResult = null; if (FEATURES.useTranslateCache) { cacheResult = TranslateCacheService.queryTranslateCache(inputValue); } if (cacheResult) { let resultText = ''; let tipMessage = ''; let useCache = true; // 根据缓存匹配类型处理 if (cacheResult.type === 'source') { // 命中原文,返回译文 resultText = cacheResult.translatedText; tipMessage = '译文'; } else if (cacheResult.type === 'translated') { // 命中译文,返回原文 resultText = cacheResult.sourceText; tipMessage = '原文'; } // 更新输入框内容并添加高亮效果 this.updateInputWithHighlight(widget, resultText); // 添加翻译结果到历史记录 HistoryCacheService.addHistory({ workflow_id: widget.nodeInfo?.workflow_id || '', node_id: widget.nodeId, input_id: widget.inputId, content: resultText, operation_type: 'translate', timestamp: Date.now() }); // 重置撤销状态 HistoryCacheService.initUndoState(widget.nodeId, widget.inputId, resultText); // 更新按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); return { success: true, useCache: useCache, tipType: 'info', tipMessage: tipMessage, buttonElement: e.currentTarget // 传递按钮元素 }; } // 缓存未命中,使用API翻译 // 获取翻译类型 const translateType = app.settings?.["PromptAssistant.Settings.TranslateType"] || "baidu"; // 生成唯一request_id,根据翻译类型生成对应的前缀 const request_id = generateRequestId(null, translateType); // 通知UI可以准备取消操作了 notifyCancelReady(request_id); // 检测语言 const langResult = PromptFormatter.detectLanguage(inputValue); // 根据翻译类型选择服务 let result; try { if (translateType === "baidu") { // 使用百度翻译服务 result = await APIService.baiduTranslate( inputValue, langResult.from, langResult.to, request_id ); } else { // 使用LLM翻译服务 result = await APIService.llmTranslate( inputValue, langResult.from, langResult.to, request_id ); } if (!result) { throw new Error('翻译服务返回空结果'); } } catch (error) { logger.error(`翻译失败 | 服务:${translateType} | 错误:${error.message}`); throw new Error(`翻译失败: ${error.message}`); } if (result.success) { // 格式化翻译结果(转换标点符号) const formattedText = PromptFormatter.formatTranslatedText(result.data.translated); // 添加翻译结果到历史记录 HistoryCacheService.addHistory({ workflow_id: widget.nodeInfo?.workflow_id || '', node_id: widget.nodeId, input_id: widget.inputId, content: formattedText, operation_type: 'translate', request_id: request_id, timestamp: Date.now() }); // 更新输入框内容并添加高亮效果 this.updateInputWithHighlight(widget, formattedText); // 重置撤销状态 HistoryCacheService.initUndoState(widget.nodeId, widget.inputId, formattedText); // 更新按钮状态 UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); // 只有开启缓存时才写入缓存 if (FEATURES.useTranslateCache) { TranslateCacheService.addTranslateCache(inputValue, formattedText); } return { success: true, useCache: false, tipType: 'success', tipMessage: '翻译完成' }; } else { // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理 throw new Error(result.error); } } catch (error) { // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理 throw error; } } ); }, visible: FEATURES.translate, // Note节点只显示此按钮 // 添加右键菜单配置 contextMenu: async (widget) => { const currentTranslateType = app.ui.settings.getSettingValue("PromptAssistant.Settings.TranslateType", "baidu"); const useTranslateCache = app.ui.settings.getSettingValue("PromptAssistant.Features.UseTranslateCache", true); // 获取LLM提供商列表 let llmProviders = {}; let currentLLMProvider = null; try { const resp = await fetch('/prompt_assistant/api/config/llm'); if (resp.ok) { const cfg = await resp.json(); llmProviders = cfg.providers || {}; currentLLMProvider = cfg.provider || null; } } catch (e) { logger.error(`获取LLM提供商失败: ${e.message}`); } const providerNameMap = { zhipu: '智谱', siliconflow: '硅基流动', "302ai": '302.AI', ollama: 'Ollama', custom: '自定义' }; const order = ['zhipu', 'siliconflow', '302ai', 'ollama', 'custom']; const providerChildren = order .filter(key => Object.prototype.hasOwnProperty.call(llmProviders, key)) .map(key => ({ label: providerNameMap[key] || key, icon: ``, onClick: async (context) => { try { const res = await fetch('/prompt_assistant/api/config/llm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_provider: key }) }); if (!res.ok) throw new Error(`服务器返回错误: ${res.status}`); // 确保切换到LLM翻译 app.ui.settings.setSettingValue("PromptAssistant.Settings.TranslateType", "llm"); UIToolkit.showStatusTip(context.buttonElement, 'success', `已切换到: ${providerNameMap[key] || key}`); } catch (err) { logger.error(`切换LLM提供商失败: ${err.message}`); UIToolkit.showStatusTip(context.buttonElement, 'error', `切换失败: ${err.message}`); } } })); return [ { label: "使用百度翻译", icon: ``, onClick: (context) => { logger.log("右键菜单 | 动作: 切换到百度翻译"); app.ui.settings.setSettingValue("PromptAssistant.Settings.TranslateType", "baidu"); UIToolkit.showStatusTip(context.buttonElement, 'success', `已切换到: 百度翻译`); } }, { label: "使用大语言模型翻译", icon: ``, onClick: (context) => { logger.log("右键菜单 | 动作: 切换到LLM翻译"); app.ui.settings.setSettingValue("PromptAssistant.Settings.TranslateType", "llm"); UIToolkit.showStatusTip(context.buttonElement, 'success', `已切换到: 大语言模型翻译`); }, children: providerChildren }, { type: 'separator' }, { label: "翻译缓存", icon: ``, onClick: (context) => { const newStatus = !useTranslateCache; app.ui.settings.setSettingValue("PromptAssistant.Features.UseTranslateCache", newStatus); const statusText = newStatus ? '已开启' : '已关闭'; logger.log(`右键菜单 | 动作: 切换翻译缓存 | 状态: ${statusText}`); UIToolkit.showStatusTip(context.buttonElement, 'success', `翻译缓存${statusText}`); } } ]; } }, ]; // 记录添加的按钮 let historyButtons = []; let otherButtons = []; let divider = null; // ---使用for...of循环支持异步操作--- for (const config of buttonConfigs) { if (config.type === 'divider') { // 创建分割线但暂不添加 divider = document.createElement('div'); divider.className = 'prompt-assistant-divider'; divider.dataset.id = config.id || `divider_${Date.now()}`; // 保存引用 if (config.id) { widget.buttons[config.id] = divider; } continue; } // 检查按钮是否可见 if (config.visible === false) { // 区分节点类型和功能禁用的情况 const reason = (widget.nodeInfo?.isNoteNode && config.id !== 'translate') ? "Note节点限制" : "功能禁用"; // logger.debug(`按钮跳过 | 按钮: ${config.id || 'unknown'} | 原因: ${reason}`); continue; } // ---异步创建按钮--- const button = this.addButtonWithIcon(widget, config); if (!button) continue; // 设置初始状态 if (config.initialState) { Object.entries(config.initialState).forEach(([stateType, value]) => { UIToolkit.setButtonState(widget, config.id, stateType, value); }); } // 根据按钮类型分组 if (['history', 'undo', 'redo'].includes(config.id)) { historyButtons.push(button); } else { otherButtons.push(button); } } // 添加按钮到DOM,并在需要时添加分割线 let addedButtonCount = 0; // 添加历史相关按钮 historyButtons.forEach(button => { widget.innerContent.appendChild(button); addedButtonCount++; }); // 如果两种类型的按钮都存在,添加分割线 if (historyButtons.length > 0 && otherButtons.length > 0 && divider) { widget.innerContent.appendChild(divider); } // 添加其他按钮 otherButtons.forEach(button => { widget.innerContent.appendChild(button); addedButtonCount++; }); // 判断节点类型并记录日志 if (widget.nodeInfo?.isNoteNode) { logger.debug(`按钮添加 | 结果:完成 | 节点ID: ${widget.nodeId} | Note节点: 仅显示翻译按钮 | 添加数量: ${addedButtonCount}`); } else { logger.debug(`按钮添加 | 结果:完成 | 节点ID: ${widget.nodeId} | 标准节点 | 添加数量: ${addedButtonCount}`); } } /** * 添加带图标的按钮 */ addButtonWithIcon(widget, config) { if (!widget?.element || !widget?.innerContent) return null; const { id, title, icon, onClick, contextMenu } = config; // 创建按钮 const button = document.createElement('button'); button.className = 'prompt-assistant-button'; button.title = title || ''; button.dataset.id = id || `btn_${Date.now()}`; // 添加图标 - 使用UIToolkit的SVG图标方法 if (icon) { UIToolkit.addIconToButton(button, icon, title || ''); } // 添加事件 if (typeof onClick === 'function') { button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 如果按钮被禁用,不执行操作 if (button.classList.contains('button-disabled')) { return; } // 执行点击回调 onClick(e, widget); }); } // 添加右键菜单(如果有) if (contextMenu && typeof contextMenu === 'function') { this._setupButtonContextMenu(button, contextMenu, widget); } // 保存引用 if (id) { widget.buttons[id] = button; } return button; } /** * 检测输入框是否有滚动条 * @param {HTMLElement} inputEl - 输入框元素 * @returns {boolean} 是否有垂直滚动条 */ _detectScrollbar(inputEl) { if (!inputEl || inputEl.tagName !== 'TEXTAREA') { return false; } try { // 检查垂直滚动条:scrollHeight > clientHeight const hasVerticalScrollbar = inputEl.scrollHeight > inputEl.clientHeight; // 日志简化:详细滚动条检测日志移至 _adjustPositionForScrollbar,并仅在状态变更时输出 return hasVerticalScrollbar; } catch (error) { logger.error(`[滚动条检测] 检测失败 | 错误: ${error.message}`); return false; } } /** * 根据滚动条状态调整小助手位置 * @param {Object} widget - 小助手实例 * @param {HTMLElement} inputEl - 输入框元素 */ _adjustPositionForScrollbar(widget, inputEl) { if (!widget?.element || !inputEl) return; const hasScrollbar = this._detectScrollbar(inputEl); const containerDiv = widget.element; // 仅在滚动条状态发生变化时更新位置与输出日志 const prevState = containerDiv.dataset.hasScrollbar === 'true'; if (prevState === hasScrollbar) { return; // 状态未变,不做任何操作 } containerDiv.dataset.hasScrollbar = String(hasScrollbar); // 检查当前定位模式 const isStandardMode = containerDiv.style.position !== 'fixed'; if (isStandardMode) { // ---标准定位模式调整--- const rightOffset = hasScrollbar ? '16px' : '4px'; // 有滚动条时向左偏移10px containerDiv.style.right = rightOffset; logger.debug(`[位置调整] 标准方案 | 滚动条: ${hasScrollbar} → 偏移: ${rightOffset}`); } else { // ---兼容定位模式调整--- // 需要重新计算位置,因为固定定位使用的是left属性 const inputRect = inputEl.getBoundingClientRect(); const offsetRight = hasScrollbar ? 18 : 6; // 有滚动条时增加10px偏移 // 临时隐藏以获取正确尺寸 containerDiv.style.visibility = 'hidden'; void containerDiv.offsetWidth; // 计算新位置 const newLeft = inputRect.right - containerDiv.offsetWidth - offsetRight; containerDiv.style.left = `${newLeft}px`; containerDiv.style.visibility = 'visible'; logger.debug(`[位置调整] 兼容方案 | 滚动条: ${hasScrollbar} → 左:${newLeft}px`); } } /** * 设置UI位置 * 使用绝对定位方式 */ _setupUIPosition(widget, inputEl, containerDiv, canvasContainerRect) { const _applyStandardPositioning = (containerDiv, domWidgetContainer) => { logger.log("定位方式 - 标准方案"); // 标准模式使用默认的绝对定位,添加位置参数 containerDiv.style.right = '4px'; containerDiv.style.bottom = '4px'; // 直接添加到dom-widget容器,但不修改dom-widget本身 domWidgetContainer.appendChild(containerDiv); // 触发回流,确保样式更新 void containerDiv.offsetWidth; // 强制应用容器高度 containerDiv.style.height = '20px'; containerDiv.style.minHeight = '20px'; void containerDiv.offsetWidth; // 触发回流,确保样式应用 }; // 查找dom-widget父容器(ComfyUI 0.3.27及以上版本的标准容器) const findDomWidgetContainer = () => { let domWidgetContainer = null; // 向上查找dom-widget容器 let parent = inputEl.parentElement; while (parent) { if (parent.classList && parent.classList.contains('dom-widget')) { domWidgetContainer = parent; break; } parent = parent.parentElement; } return domWidgetContainer; }; // 初始查找dom-widget容器 let domWidgetContainer = findDomWidgetContainer(); // 清理函数列表 widget._eventCleanupFunctions = widget._eventCleanupFunctions || []; // 检查是否需要使用兼容模式 let needCompatibilityMode = !domWidgetContainer; // 如果初次判断需要使用兼容模式,添加延迟重试机制 // 这是为了处理新创建节点时DOM可能尚未完全渲染的情况 if (needCompatibilityMode) { // 先创建一个临时容器,避免显示延迟 const tempDiv = document.createElement('div'); tempDiv.className = 'prompt-assistant-container prompt-assistant-temp'; tempDiv.style.display = 'none'; document.body.appendChild(tempDiv); // 保存临时容器引用,以便后续移除 widget._tempContainer = tempDiv; // 设置重试次数和间隔 const maxRetries = 2; const retryInterval = 500; // 毫秒 let retryCount = 0; // 创建重试函数 const retrySetupPosition = () => { // 重新查找dom-widget容器 domWidgetContainer = findDomWidgetContainer(); if (domWidgetContainer) { // 找到了dom-widget容器,使用标准定位方案 logger.log("定位方式 - 延迟检测后使用标准方案"); needCompatibilityMode = false; // 移除临时容器 if (widget._tempContainer && document.body.contains(widget._tempContainer)) { document.body.removeChild(widget._tempContainer); delete widget._tempContainer; } // 使用标准定位方案 _applyStandardPositioning(containerDiv, domWidgetContainer); return true; } else if (retryCount < maxRetries) { // 继续重试 retryCount++; logger.debug(`定位方式 - 重试查找dom-widget容器 (${retryCount}/${maxRetries})`); setTimeout(retrySetupPosition, retryInterval); return false; } else { // 达到最大重试次数,使用兼容定位方案 logger.log("定位方式 - 重试失败,使用兼容方案"); // 移除临时容器 if (widget._tempContainer && document.body.contains(widget._tempContainer)) { document.body.removeChild(widget._tempContainer); delete widget._tempContainer; } // 使用兼容定位方案 setupCompatibilityMode(); return true; } }; // 定义兼容模式设置函数 const setupCompatibilityMode = () => { // === 兼容方案定位 (ComfyUI 0.3.26及以下) === logger.log("定位方式 - 兼容方案"); // 使用固定定位样式,不添加额外类 containerDiv.style.position = 'fixed'; containerDiv.style.zIndex = '9999'; document.body.appendChild(containerDiv); // 设置初始显示 containerDiv.style.display = 'flex'; // 更新位置的函数,确保跟随输入框并位于右下角 const updatePosition = () => { if (!widget.element || !inputEl || !containerDiv) return; try { // 获取输入框的位置信息 const inputRect = inputEl.getBoundingClientRect(); // 设置容器样式 - 首先确保容器可见以便获得正确的尺寸 containerDiv.style.display = 'flex'; containerDiv.style.visibility = 'hidden'; // 暂时隐藏以避免闪烁 // 先设置样式,使其能正确计算尺寸 Object.assign(containerDiv.style, { width: 'auto', height: '24px', pointerEvents: 'none' }); // 确保小部件样式正确 Object.assign(widget.element.style, { transformOrigin: 'right center', margin: '0', pointerEvents: 'auto' }); // 强制回流以获取正确尺寸 void containerDiv.offsetWidth; // 现在设置位置 - 放置在输入框右下角,考虑滚动条状态 const hasScrollbar = inputEl.tagName === 'TEXTAREA' && inputEl.scrollHeight > inputEl.clientHeight; const offsetRight = hasScrollbar ? 16 : 6; // 有滚动条时增加10px偏移 const offsetBottom = 4; containerDiv.style.left = `${inputRect.right - containerDiv.offsetWidth - offsetRight}px`; containerDiv.style.top = `${inputRect.bottom - containerDiv.offsetHeight - offsetBottom}px`; containerDiv.style.visibility = 'visible'; // 恢复可见性 // 最终触发回流,确保样式更新 void containerDiv.offsetWidth; } catch (error) { logger.error("更新小助手位置出错:", error); // 错误恢复 - 确保组件仍然可见 if (containerDiv) containerDiv.style.visibility = 'visible'; } }; // 初始更新位置 updatePosition(); // 使用防抖函数优化位置更新,但降低延迟提高流畅度 const debouncedUpdatePosition = EventManager.debounce(updatePosition, 16); // 添加窗口resize事件监听 const removeResizeListener = EventManager.addDOMListener(window, 'resize', debouncedUpdatePosition); widget._eventCleanupFunctions.push(removeResizeListener); // 监听画布变化 const app = window.app || null; if (!app || !app.canvas) { logger.error("错误:无法获取app对象,小助手无法正常工作"); return; } // 1. 使用MutationObserver监听画布变化 const observer = new MutationObserver(debouncedUpdatePosition); const canvasParent = app.canvas.canvas.parentElement; if (canvasParent) { observer.observe(canvasParent, { attributes: true, attributeFilter: ['style', 'transform'] }); // 添加Observer清理函数 widget._eventCleanupFunctions.push(() => observer.disconnect()); } // 2. 监听画布重绘 - 使用直接更新而不是防抖版本,确保重绘时位置准确 const originalDrawBackground = app.canvas.onDrawBackground; app.canvas.onDrawBackground = function () { const ret = originalDrawBackground?.apply(this, arguments); // 直接调用updatePosition而不是防抖版本 updatePosition(); return ret; }; // 添加画布重绘清理函数 widget._eventCleanupFunctions.push(() => { if (originalDrawBackground) { app.canvas.onDrawBackground = originalDrawBackground; } }); // 3. 监听节点移动 - 使用 nodeInfo 而不是直接的 node 引用 const nodeId = widget.nodeInfo?.nodeId; if (nodeId && app.canvas && app.canvas.graph) { const node = app.canvas.graph.getNodeById(nodeId); if (node) { // 使用LiteGraph提供的onNodeMoved事件 const originalOnNodeMoved = app.canvas.onNodeMoved; app.canvas.onNodeMoved = function (node_dragged) { if (originalOnNodeMoved) { originalOnNodeMoved.apply(this, arguments); } // 仅当移动的是当前节点时更新位置 if (node_dragged && node_dragged.id === nodeId) { // 直接调用updatePosition而不是防抖版本,确保拖动时UI跟随节点 updatePosition(); } }; // 添加节点移动清理函数 widget._eventCleanupFunctions.push(() => { if (app.canvas) { app.canvas.onNodeMoved = originalOnNodeMoved; } }); // 为节点本身添加移动监听(兼容性处理) const nodeOriginalOnNodeMoved = node.onNodeMoved; node.onNodeMoved = function () { const ret = nodeOriginalOnNodeMoved?.apply(this, arguments); // 直接调用updatePosition而不是防抖版本 updatePosition(); return ret; }; // 添加节点自身移动清理函数 widget._eventCleanupFunctions.push(() => { if (node && nodeOriginalOnNodeMoved) { node.onNodeMoved = nodeOriginalOnNodeMoved; } }); } } // 统一的画布事件处理函数 const handleCanvasEvent = () => { if (widget.element && widget.element.style.display !== 'none') { updatePosition(); } }; // 监听画布相关事件 if (!app || !app.canvas) { logger.error("错误:无法获取app对象,小助手无法正常工作"); return; } // 保存原始方法引用 const originalMethods = { setDirty: app.canvas.setDirty, drawBackground: app.canvas.onDrawBackground, dsModified: app.canvas.ds.onModified, setTransform: app.canvas.ds.setTransform, resize: app.canvas.resize }; // 1. 监听画布变换(包括缩放、平移等) app.canvas.setDirty = function (value, skipEvents) { const ret = originalMethods.setDirty?.apply(this, arguments); if (!skipEvents) { handleCanvasEvent(); } return ret; }; // 2. 监听画布重绘和缩放 app.canvas.onDrawBackground = function () { const ret = originalMethods.drawBackground?.apply(this, arguments); handleCanvasEvent(); return ret; }; app.canvas.ds.onModified = function (...args) { if (originalMethods.dsModified) { originalMethods.dsModified.apply(this, args); } handleCanvasEvent(); }; app.canvas.ds.setTransform = function () { const ret = originalMethods.setTransform?.apply(this, arguments); handleCanvasEvent(); return ret; }; // 3. 监听画布大小变化 app.canvas.resize = function () { const ret = originalMethods.resize?.apply(this, arguments); handleCanvasEvent(); return ret; }; // 4. 使用MutationObserver监听画布容器变化 const canvasContainer = app.canvas.canvas.parentElement; if (canvasContainer) { const observer = new MutationObserver(() => handleCanvasEvent()); observer.observe(canvasContainer, { attributes: true, attributeFilter: ['style', 'transform'] }); widget._eventCleanupFunctions.push(() => observer.disconnect()); } // 5. 使用requestAnimationFrame实现平滑更新 let rafId = null; const smoothUpdate = () => { handleCanvasEvent(); rafId = requestAnimationFrame(smoothUpdate); }; rafId = requestAnimationFrame(smoothUpdate); // 添加所有清理函数 widget._eventCleanupFunctions.push( () => { // 清理画布相关事件监听 if (app.canvas) { app.canvas.setDirty = originalMethods.setDirty; app.canvas.onDrawBackground = originalMethods.drawBackground; app.canvas.resize = originalMethods.resize; } if (app.canvas?.ds) { app.canvas.ds.onModified = originalMethods.dsModified; app.canvas.ds.setTransform = originalMethods.setTransform; } // 清理requestAnimationFrame if (rafId !== null) { cancelAnimationFrame(rafId); } } ); // 添加DOM元素清理函数 widget._eventCleanupFunctions.push(() => { // 移除DOM元素 if (containerDiv && document.body.contains(containerDiv)) { document.body.removeChild(containerDiv); } }); // 强制应用容器高度 containerDiv.style.height = '20px'; containerDiv.style.minHeight = '20px'; void containerDiv.offsetWidth; // 触发回流,确保样式应用 }; // 开始重试流程 setTimeout(retrySetupPosition, retryInterval); } else { // === 标准方案定位 (ComfyUI 0.3.27及以上) === _applyStandardPositioning(containerDiv, domWidgetContainer); } } /** * 清理单个实例的资源 */ _cleanupInstance(instance, instanceKey, skipRemove = false) { try { // 检查实例是否有效 if (!instance) { logger.debug(`实例清理 | 结果:跳过 | 实例:${instanceKey || 'unknown'} | 原因:实例不存在`); return; } // 1. 重置所有按钮状态 if (instance.buttons) { Object.keys(instance.buttons).forEach(buttonId => { try { const button = instance.buttons[buttonId]; if (button) { // 移除所有状态类 button.classList.remove('button-active', 'button-processing', 'button-disabled'); // 移除所有事件监听器 button.replaceWith(button.cloneNode(true)); } } catch (err) { logger.debug(`按钮清理 | 按钮:${buttonId} | 错误:${err.message}`); } }); // 清空按钮引用 instance.buttons = {}; } // 2. 清理事件监听器 if (instance.cleanupListeners && typeof instance.cleanupListeners === 'function') { try { instance.cleanupListeners(); } catch (err) { logger.debug(`监听器清理 | 错误:${err.message}`); } } // 3. 清理所有保存的事件清理函数 if (instance._eventCleanupFunctions && Array.isArray(instance._eventCleanupFunctions)) { instance._eventCleanupFunctions.forEach(cleanup => { if (typeof cleanup === 'function') { try { cleanup(); } catch (err) { logger.debug(`事件清理 | 错误:${err.message}`); } } }); instance._eventCleanupFunctions = []; } // 4. 从DOM中移除元素 if (instance.element) { try { // 确保在移除元素前清理所有子元素的事件 const allButtons = instance.element.querySelectorAll('button'); allButtons.forEach(button => { button.replaceWith(button.cloneNode(true)); }); // 清理指示器元素 if (instance.indicator && instance.indicator.parentNode) { instance.indicator.innerHTML = ''; } if (instance.element.parentNode) { instance.element.parentNode.removeChild(instance.element); } } catch (err) { logger.debug(`DOM元素清理 | 错误:${err.message}`); } } // 5. 清理输入框映射 if (window.PromptAssistantInputWidgetMap && instanceKey) { try { delete window.PromptAssistantInputWidgetMap[instanceKey]; } catch (err) { logger.debug(`输入框映射清理 | 错误:${err.message}`); } } // 6. 清理弹窗状态 if (window.FEATURES && window.FEATURES.updateButtonsVisibility) { try { window.FEATURES.updateButtonsVisibility(); } catch (err) { logger.debug(`按钮可见性更新 | 错误:${err.message}`); } } // 7. 从实例集合中移除(除非明确指定跳过) if (!skipRemove && instanceKey) { try { PromptAssistant.instances.delete(instanceKey); } catch (err) { logger.debug(`实例集合清理 | 错误:${err.message}`); } } // 8. 清理实例属性 try { Object.keys(instance).forEach(key => { try { delete instance[key]; } catch (err) { logger.debug(`属性清理 | 属性:${key} | 错误:${err.message}`); } }); } catch (err) { logger.debug(`属性清理 | 错误:${err.message}`); } logger.debug(`实例清理 | 结果:成功 | 实例:${instanceKey || 'unknown'}`); } catch (error) { logger.error(`实例清理失败 | 实例:${instanceKey || 'unknown'} | 错误:${error.message}`); } } /** * 设置按钮右键菜单 * @param {HTMLElement} button 按钮元素 * @param {Function} getMenuItems 获取菜单项的函数 * @param {Object} widget 小助手实例 */ _setupButtonContextMenu(button, getMenuItems, widget) { if (!button || typeof getMenuItems !== 'function') return; // 设置右键菜单 const cleanup = buttonMenu.setupButtonMenu(button, () => { // 调用getMenuItems函数获取菜单项,传入widget作为上下文 return getMenuItems(widget); }, { widget, buttonElement: button }); // 保存清理函数到widget的事件清理函数列表中 if (cleanup) { widget._eventCleanupFunctions = widget._eventCleanupFunctions || []; widget._eventCleanupFunctions.push(cleanup); } } } // 创建单例实例 const promptAssistant = new PromptAssistant(); // 导出 export { promptAssistant, PromptAssistant };