1798 lines
70 KiB
JavaScript
1798 lines
70 KiB
JavaScript
/**
|
|
* 提示词小助手核心类
|
|
* 统一管理小助手的生命周期、实例创建、UI交互等功能
|
|
*/
|
|
|
|
import { app } from "../../../../scripts/app.js";
|
|
import { logger } from '../utils/logger.js';
|
|
import { FEATURES } from "../config/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 { BaiduTranslateService } from "../services/baidu.js";
|
|
import { llmService } from "../services/llm.js";
|
|
|
|
// ====================== 工具函数 ======================
|
|
|
|
/**
|
|
* 生成唯一请求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.log(`总开关状态 | 状态:${window.FEATURES.enabled ? "启用" : "禁用"}`);
|
|
|
|
// 迁移旧的缓存数据到新的键名格式
|
|
CacheService.migrateCache();
|
|
|
|
// 初始化资源管理器
|
|
ResourceManager.init();
|
|
|
|
// 只有在总开关打开时才做完整初始化
|
|
if (window.FEATURES.enabled) {
|
|
// 注册全局鼠标监听
|
|
this.registerGlobalMouseListener();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 3. 注册全局鼠标监听
|
|
await this.registerGlobalMouseListener();
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
// 3. 移除全局鼠标监听
|
|
this.removeGlobalMouseListener();
|
|
}
|
|
|
|
// 按钮可见性更新在features中单独处理
|
|
window.FEATURES.updateButtonsVisibility();
|
|
} catch (error) {
|
|
logger.error(`总开关操作失败 | 错误: ${error.message}`);
|
|
// 恢复原始状态
|
|
window.FEATURES.enabled = oldValue;
|
|
}
|
|
}
|
|
|
|
// ---资源管理功能---
|
|
/**
|
|
* 清理所有资源
|
|
*/
|
|
cleanup(nodeId = null, silent = false) {
|
|
// 清理特定节点
|
|
if (nodeId) {
|
|
// 获取当前节点的所有实例键
|
|
const keysToDelete = Array.from(PromptAssistant.instances.keys())
|
|
.filter(key => key === String(nodeId) || key.startsWith(`${nodeId}_`));
|
|
|
|
// 如果有实例需要清理
|
|
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 remainingTags = Object.keys(localStorage)
|
|
.filter(key => key.startsWith(CACHE_CONFIG.TAG_KEY_PREFIX))
|
|
.reduce((total, key) => {
|
|
try {
|
|
const cacheData = JSON.parse(localStorage.getItem(key));
|
|
return total + (cacheData ? Object.keys(cacheData).length : 0);
|
|
} catch (e) {
|
|
return total;
|
|
}
|
|
}, 0);
|
|
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();
|
|
|
|
// 统计并清理所有标签缓存和实例
|
|
for (const [key, instance] of PromptAssistant.instances) {
|
|
if (instance && instance.nodeId && instance.inputId) {
|
|
const tags = TagCacheService.getAllRawTags(instance.nodeId, instance.inputId);
|
|
totalTagCount += tags ? tags.length : 0;
|
|
TagCacheService.clearCache(instance.nodeId, instance.inputId);
|
|
allInstanceNames.push(instance.inputId);
|
|
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 || !PromptAssistant.isValidNode(node) || (node.flags && node.flags.collapsed)) {
|
|
return;
|
|
}
|
|
|
|
// 查找有效的文本输入控件
|
|
const validInputs = [];
|
|
if (node.widgets) {
|
|
node.widgets.forEach(widget => {
|
|
// 不再直接设置 node 引用,因为它现在是只读的
|
|
if (UIToolkit.isValidInput(widget)) {
|
|
validInputs.push(widget);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 如果没有找到有效的文本输入控件,直接返回
|
|
if (validInputs.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// 为每个有效控件创建小助手
|
|
validInputs.forEach(inputWidget => {
|
|
const inputId = inputWidget.name || inputWidget.id;
|
|
const assistantKey = `${node.id}_${inputId}`;
|
|
|
|
// 检查实例是否已存在
|
|
if (PromptAssistant.hasInstance(assistantKey)) {
|
|
return; // 跳过已存在的实例
|
|
}
|
|
|
|
// 再次检查总开关状态,确保在创建过程中没有被禁用
|
|
if (!window.FEATURES.enabled) {
|
|
return;
|
|
}
|
|
|
|
// 创建小助手实例
|
|
const assistant = this.setupNodeAssistant(node, inputWidget);
|
|
if (assistant) {
|
|
logger.log(`创建小助手 | 节点:${node.id} | 控件:${inputId} | 实例:${assistantKey}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 为节点设置小助手
|
|
* 创建小助手实例并初始化显示状态
|
|
*/
|
|
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 = {
|
|
nodeType: node.type,
|
|
inputType: inputId,
|
|
isNoteNode: isNoteNode
|
|
};
|
|
|
|
// 创建小助手实例
|
|
const assistant = this.createAssistant(
|
|
node,
|
|
inputId,
|
|
isNoteNode ? { ...inputWidget, inputEl: inputWidget.element } : inputWidget, // Note节点特殊处理
|
|
nodeInfo
|
|
);
|
|
|
|
if (assistant) {
|
|
// 初始化显示状态
|
|
assistant.isFirstCreate = true;
|
|
this.showAssistantUI(assistant);
|
|
|
|
// 设置初始悬停状态,2秒后恢复正常行为
|
|
assistant.isMouseOver = true;
|
|
setTimeout(() => {
|
|
if (assistant) {
|
|
assistant.isFirstCreate = false;
|
|
if (!assistant.isMouseOver) {
|
|
this.updateAssistantVisibility(assistant);
|
|
}
|
|
}
|
|
}, 2000);
|
|
|
|
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
|
|
},
|
|
isMouseOver: false, // 鼠标悬停状态
|
|
isFirstCreate: false, // 首次创建标记
|
|
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);
|
|
});
|
|
widget._eventCleanupFunctions.push(removeInputListener);
|
|
}
|
|
|
|
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';
|
|
|
|
// 创建主容器 - 简化样式,减少触发栅格化的属性
|
|
const containerDiv = document.createElement('div');
|
|
containerDiv.className = 'prompt-assistant-container';
|
|
containerDiv.dataset.nodeId = nodeId;
|
|
containerDiv.dataset.inputId = inputId;
|
|
|
|
// 添加内容容器到主容器
|
|
containerDiv.appendChild(innerContentDiv);
|
|
|
|
// 保存引用并初始化
|
|
widget.element = containerDiv;
|
|
widget.innerContent = innerContentDiv;
|
|
widget.buttons = {};
|
|
|
|
// 初始化UI组件和事件
|
|
this.addFunctionButtons(widget);
|
|
this._setupUIEventHandling(widget, inputEl, containerDiv);
|
|
this._setupUIPosition(widget, inputEl, containerDiv, canvasContainerRect);
|
|
|
|
// 默认隐藏状态
|
|
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;
|
|
}
|
|
|
|
// 取消任何隐藏计时器
|
|
if (widget.hideTimeout) {
|
|
clearTimeout(widget.hideTimeout);
|
|
widget.hideTimeout = null;
|
|
}
|
|
|
|
// 设置过渡状态
|
|
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.remove('assistant-hide');
|
|
widget.element.classList.add('assistant-show');
|
|
|
|
// 动画结束后重置过渡状态
|
|
setTimeout(() => {
|
|
widget.isTransitioning = false;
|
|
// 动画结束后检查鼠标状态
|
|
setTimeout(() => this.forceUpdateMouseState(widget), 10);
|
|
}, 300); // 与CSS动画时长匹配
|
|
}
|
|
|
|
/**
|
|
* 隐藏小助手UI
|
|
* 控制UI隐藏动画和状态
|
|
*/
|
|
hideAssistantUI(widget) {
|
|
if (!widget?.element) return;
|
|
|
|
// 避免重复隐藏
|
|
if (!widget.element.classList.contains('assistant-show')) return;
|
|
|
|
// 设置过渡状态
|
|
widget.isTransitioning = true;
|
|
|
|
// 应用隐藏动画类
|
|
widget.element.classList.add('assistant-hide');
|
|
widget.element.classList.remove('assistant-show');
|
|
|
|
// 触发回流确保动画生效
|
|
void widget.element.offsetWidth;
|
|
|
|
// 动画结束后隐藏元素
|
|
widget.hideTimeout = setTimeout(() => {
|
|
if (widget.element) {
|
|
widget.element.style.display = 'none';
|
|
}
|
|
|
|
// 重置过渡状态
|
|
widget.isTransitioning = false;
|
|
|
|
// 动画结束后检查鼠标状态
|
|
setTimeout(() => this.forceUpdateMouseState(widget), 10);
|
|
}, 300); // 与CSS动画时长匹配
|
|
}
|
|
|
|
/**
|
|
* 更新小助手可见性
|
|
* 根据鼠标悬停和首次创建状态决定是否显示
|
|
*/
|
|
updateAssistantVisibility(widget) {
|
|
if (!widget) return;
|
|
|
|
// 总开关关闭时不处理可见性更新
|
|
if (!window.FEATURES || !window.FEATURES.enabled) {
|
|
return;
|
|
}
|
|
|
|
// 检查是否有按钮处于激活或处理中状态
|
|
const hasActiveButtons = this._checkAssistantActiveState(widget);
|
|
|
|
// 如果有激活的按钮,强制显示小助手(带动画)
|
|
if (hasActiveButtons) {
|
|
this.showAssistantUI(widget, true);
|
|
return;
|
|
}
|
|
|
|
const isMouseOver = widget.isMouseOver === true;
|
|
const isFirstCreate = widget.isFirstCreate === true;
|
|
|
|
// 显示条件:首次创建或鼠标悬停
|
|
const shouldShow = isFirstCreate || isMouseOver;
|
|
const isCurrentlyShown = widget.element?.classList.contains('assistant-show');
|
|
|
|
// 仅在状态需变化时更新
|
|
if (shouldShow !== isCurrentlyShown) {
|
|
const reason = isFirstCreate ? "首次创建" : (isMouseOver ? "鼠标悬停" : "鼠标离开");
|
|
|
|
if (shouldShow) {
|
|
// 首次创建时使用动画,鼠标悬停时不使用动画
|
|
this.showAssistantUI(widget, isFirstCreate);
|
|
logger.debug(`UI显示 | 节点:${widget.nodeId} | 原因:${reason}`);
|
|
} else {
|
|
this.hideAssistantUI(widget);
|
|
logger.debug(`UI隐藏 | 节点:${widget.nodeId} | 原因:${reason}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查小助手是否有按钮处于激活状态
|
|
*/
|
|
_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 => {
|
|
if (!widget.isTransitioning) {
|
|
this.updateAssistantVisibility(widget);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 显示状态提示
|
|
* 创建临时提示信息气泡
|
|
*/
|
|
showStatusTip(anchorElement, type, message, position = null) {
|
|
return UIToolkit.showStatusTip(anchorElement, type, message, position);
|
|
}
|
|
|
|
// ---事件处理功能---
|
|
/**
|
|
* 设置UI事件处理
|
|
* 配置鼠标悬停相关的事件监听 - 简化版本
|
|
*/
|
|
_setupUIEventHandling(widget, inputEl, containerDiv) {
|
|
// 设置容器的事件穿透
|
|
containerDiv.style.pointerEvents = 'none';
|
|
widget.innerContent.style.pointerEvents = 'auto'; // 确保内容容器可以接收事件
|
|
|
|
// 确保所有按钮可点击
|
|
const buttons = containerDiv.querySelectorAll('button');
|
|
buttons.forEach(button => {
|
|
button.style.pointerEvents = 'auto';
|
|
button.classList.add('prompt-assistant-button-active');
|
|
});
|
|
|
|
// 初始化过渡状态标记
|
|
widget.isTransitioning = false;
|
|
|
|
// 使用清理函数数组存储所有事件清理函数
|
|
widget._eventCleanupFunctions = widget._eventCleanupFunctions || [];
|
|
|
|
// 添加清理函数
|
|
widget.cleanupListeners = () => {
|
|
// 执行所有保存的清理函数
|
|
if (widget._eventCleanupFunctions && widget._eventCleanupFunctions.length) {
|
|
widget._eventCleanupFunctions.forEach(cleanupFn => {
|
|
if (typeof cleanupFn === 'function') {
|
|
cleanupFn();
|
|
}
|
|
});
|
|
widget._eventCleanupFunctions = [];
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 判断鼠标是否在元素上方
|
|
*/
|
|
isMouseOverElement(element) {
|
|
if (!element) return false;
|
|
|
|
try {
|
|
// 直接使用计算而非委托给EventManager,减少一层调用
|
|
const mousePos = EventManager.getMousePosition();
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
return (
|
|
mousePos.x >= rect.left &&
|
|
mousePos.x <= rect.right &&
|
|
mousePos.y >= rect.top &&
|
|
mousePos.y <= rect.bottom
|
|
);
|
|
} catch (error) {
|
|
logger.error(`鼠标位置检测失败 | 错误: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 强制更新鼠标悬停状态
|
|
*/
|
|
forceUpdateMouseState(widget) {
|
|
if (!widget || !widget.node || !widget.text_element || !widget.element) return;
|
|
|
|
// 总开关关闭时不处理鼠标状态更新
|
|
if (!window.FEATURES || !window.FEATURES.enabled) {
|
|
return;
|
|
}
|
|
|
|
// 使用直接检测方法检查鼠标是否在元素上方
|
|
const isOverInput = this.isMouseOverElement(widget.text_element);
|
|
const isOverContainer = widget.element.style.display !== 'none' &&
|
|
this.isMouseOverElement(widget.element);
|
|
|
|
// 更新状态
|
|
const oldState = widget.isMouseOver;
|
|
widget.isMouseOver = isOverInput || isOverContainer;
|
|
|
|
// 如果状态变化,更新可见性
|
|
if (oldState !== widget.isMouseOver) {
|
|
logger.debug(`鼠标状态 | 手动更新 | 节点:${widget.nodeId} | 原状态:${oldState} | 新状态:${widget.isMouseOver}`);
|
|
this.updateAssistantVisibility(widget);
|
|
}
|
|
|
|
return widget.isMouseOver;
|
|
}
|
|
|
|
// ---鼠标监听功能---
|
|
/**
|
|
* 判断全局鼠标监听器是否已注册
|
|
*/
|
|
isGlobalMouseListenerRegistered() {
|
|
try {
|
|
return EventManager.listeners.has('global_mouse_move') &&
|
|
EventManager.listeners.get('global_mouse_move').has('assistant_manager');
|
|
} catch (error) {
|
|
logger.error(`鼠标监听器状态检查失败 | 错误: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 注册全局鼠标移动事件监听
|
|
*/
|
|
async registerGlobalMouseListener() {
|
|
try {
|
|
// 确保EventManager已初始化
|
|
EventManager.init();
|
|
|
|
// 检查是否已注册,避免重复
|
|
if (this.isGlobalMouseListenerRegistered()) {
|
|
return true;
|
|
}
|
|
|
|
// 创建去抖动的全局监听函数
|
|
const debouncedListener = EventManager.debounce((e) => {
|
|
// 检查总开关状态,如果总开关已关闭,则不处理
|
|
if (!window.FEATURES || !window.FEATURES.enabled) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 获取当前实例数量
|
|
const instanceCount = PromptAssistant.instances.size;
|
|
if (instanceCount === 0) return;
|
|
|
|
// 遍历所有实例,更新鼠标状态
|
|
PromptAssistant.instances.forEach(widget => {
|
|
// 跳过正在过渡的实例
|
|
if (widget.isTransitioning) return;
|
|
|
|
// 检查鼠标是否在实例上
|
|
const isOver = this.isMouseOverElement(widget.text_element) ||
|
|
(widget.element?.style.display !== 'none' && this.isMouseOverElement(widget.element));
|
|
|
|
// 如果状态变化,更新可见性
|
|
if (widget.isMouseOver !== isOver) {
|
|
widget.isMouseOver = isOver;
|
|
this.updateAssistantVisibility(widget);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error(`鼠标处理异常 | 错误: ${error.message}`);
|
|
}
|
|
}, 50);
|
|
|
|
// 注册全局鼠标移动事件监听器
|
|
EventManager.on('global_mouse_move', 'assistant_manager', debouncedListener);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`全局鼠标监听注册失败 | 错误: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 移除全局鼠标移动事件监听
|
|
*/
|
|
removeGlobalMouseListener() {
|
|
try {
|
|
// 检查是否已注册,未注册则不需要移除
|
|
if (!this.isGlobalMouseListenerRegistered()) {
|
|
return true;
|
|
}
|
|
|
|
// 移除事件监听器
|
|
EventManager.off('global_mouse_move', 'assistant_manager');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`全局鼠标监听移除失败 | 错误: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---辅助功能---
|
|
/**
|
|
* 更新输入框内容并添加高亮效果
|
|
*/
|
|
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.svg',
|
|
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.svg',
|
|
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.svg',
|
|
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.svg',
|
|
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.svg',
|
|
onClick: async (e, widget) => {
|
|
logger.debug('按钮点击 | 动作: 扩写');
|
|
|
|
// 如果按钮处于 processing 状态且被点击,显示"扩写中"提示并直接返回
|
|
if (e.currentTarget.classList.contains('button-processing')) {
|
|
const btnRect = e.currentTarget.getBoundingClientRect();
|
|
UIToolkit.showStatusTip(
|
|
e.currentTarget,
|
|
'loading',
|
|
'扩写中',
|
|
{ x: btnRect.left + btnRect.width / 2, y: btnRect.top }
|
|
);
|
|
return;
|
|
}
|
|
|
|
await UIToolkit.handleAsyncButtonOperation(
|
|
widget,
|
|
'expand',
|
|
e.currentTarget,
|
|
async () => {
|
|
try {
|
|
// 获取输入值
|
|
const inputValue = widget.inputEl.value;
|
|
if (!inputValue || inputValue.trim() === '') {
|
|
throw new Error('请输入要扩写的内容');
|
|
}
|
|
|
|
// 生成唯一request_id
|
|
const request_id = generateRequestId('glm4_expand');
|
|
|
|
// 显示扩写中提示
|
|
const btnRect = e.currentTarget.getBoundingClientRect();
|
|
UIToolkit.showStatusTip(
|
|
e.currentTarget,
|
|
'loading',
|
|
'扩写中',
|
|
{ x: btnRect.left + btnRect.width / 2, y: btnRect.top }
|
|
);
|
|
|
|
// 调用LLM服务进行扩写
|
|
const result = await llmService.expandPrompt(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节点不显示此按钮
|
|
},
|
|
{
|
|
id: 'translate',
|
|
title: '翻译',
|
|
icon: 'icon-translate.svg',
|
|
onClick: async (e, widget) => {
|
|
logger.debug('按钮点击 | 动作: 翻译');
|
|
|
|
// 如果按钮处于 processing 状态且被点击,显示"翻译中"提示并直接返回
|
|
if (e.currentTarget.classList.contains('button-processing')) {
|
|
const btnRect = e.currentTarget.getBoundingClientRect();
|
|
UIToolkit.showStatusTip(
|
|
e.currentTarget,
|
|
'loading',
|
|
'翻译中',
|
|
{ x: btnRect.left + btnRect.width / 2, y: btnRect.top }
|
|
);
|
|
return;
|
|
}
|
|
|
|
await UIToolkit.handleAsyncButtonOperation(
|
|
widget,
|
|
'translate',
|
|
e.currentTarget,
|
|
async () => {
|
|
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 }
|
|
);
|
|
|
|
// 首先查询翻译缓存
|
|
const 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 = localStorage.getItem("PromptAssistant_Settings_translate_type") || "baidu";
|
|
|
|
// 生成唯一request_id,根据翻译类型生成对应的前缀
|
|
const request_id = generateRequestId(null, translateType);
|
|
|
|
// 检测语言
|
|
const langResult = PromptFormatter.detectLanguage(inputValue);
|
|
|
|
// 根据翻译类型选择服务
|
|
let result;
|
|
try {
|
|
if (translateType === "baidu") {
|
|
// 使用百度翻译服务
|
|
result = await BaiduTranslateService.translate(
|
|
inputValue,
|
|
langResult.from,
|
|
langResult.to,
|
|
request_id
|
|
);
|
|
} else {
|
|
// 使用LLM翻译服务
|
|
result = await llmService.translate(
|
|
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);
|
|
|
|
// 添加到翻译缓存
|
|
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节点只显示此按钮
|
|
},
|
|
];
|
|
|
|
// 记录添加的按钮
|
|
let historyButtons = [];
|
|
let otherButtons = [];
|
|
let divider = null;
|
|
|
|
buttonConfigs.forEach(config => {
|
|
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;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 检查按钮是否可见
|
|
if (config.visible === false) {
|
|
// 区分节点类型和功能禁用的情况
|
|
const reason = (widget.nodeInfo?.isNoteNode && config.id !== 'translate') ? "Note节点限制" : "功能禁用";
|
|
// logger.debug(`按钮跳过 | 按钮: ${config.id || 'unknown'} | 原因: ${reason}`);
|
|
return;
|
|
}
|
|
|
|
// 创建按钮
|
|
const button = this.addButtonWithIcon(widget, config);
|
|
if (!button) return;
|
|
|
|
// 设置初始状态
|
|
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) {
|
|
logger.error('添加按钮 | 结果:失败 | 原因: 容器不存在');
|
|
return null;
|
|
}
|
|
|
|
const { id, title, icon, onClick } = config;
|
|
|
|
// 创建按钮
|
|
const button = document.createElement('button');
|
|
button.className = 'prompt-assistant-button';
|
|
button.title = title || '';
|
|
button.dataset.id = id || `btn_${Date.now()}`;
|
|
button.dataset.icon = icon?.replace('.svg', ''); // 保存不带后缀的图标名,用于CSS选择器
|
|
|
|
// 按钮样式在CSS中定义
|
|
|
|
// 添加图标
|
|
if (icon) {
|
|
UIToolkit.addIconToButton(button, icon, title || id);
|
|
}
|
|
|
|
// 添加事件
|
|
if (typeof onClick === 'function') {
|
|
button.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onClick(e, widget);
|
|
});
|
|
}
|
|
|
|
// 保存引用并添加到内部内容容器
|
|
if (id) {
|
|
widget.buttons[id] = button;
|
|
}
|
|
widget.innerContent.appendChild(button);
|
|
|
|
return button;
|
|
}
|
|
|
|
/**
|
|
* 设置UI位置
|
|
* 使用绝对定位方式
|
|
*/
|
|
_setupUIPosition(widget, inputEl, containerDiv, canvasContainerRect) {
|
|
// 查找dom-widget父容器(ComfyUI 0.3.27及以上版本的标准容器)
|
|
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;
|
|
}
|
|
|
|
// 检查是否需要使用兼容模式
|
|
const needCompatibilityMode = !domWidgetContainer;
|
|
|
|
// 清理函数列表
|
|
widget._eventCleanupFunctions = widget._eventCleanupFunctions || [];
|
|
|
|
if (needCompatibilityMode) {
|
|
// === 兼容方案定位 (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 offsetRight = 12; // 右侧偏移
|
|
const offsetBottom = 8;
|
|
|
|
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, 100);
|
|
|
|
// 添加窗口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);
|
|
debouncedUpdatePosition();
|
|
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) {
|
|
const originalOnNodeMoved = node.onNodeMoved;
|
|
node.onNodeMoved = function () {
|
|
const ret = originalOnNodeMoved?.apply(this, arguments);
|
|
debouncedUpdatePosition();
|
|
return ret;
|
|
};
|
|
|
|
// 添加节点移动清理函数
|
|
widget._eventCleanupFunctions.push(() => {
|
|
if (node && originalOnNodeMoved) {
|
|
node.onNodeMoved = originalOnNodeMoved;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 添加DOM元素清理函数
|
|
widget._eventCleanupFunctions.push(() => {
|
|
// 移除DOM元素
|
|
if (containerDiv && document.body.contains(containerDiv)) {
|
|
document.body.removeChild(containerDiv);
|
|
}
|
|
});
|
|
} else {
|
|
// === 标准方案定位 (ComfyUI 0.3.27及以上) ===
|
|
logger.log("定位方式 - 标准方案");
|
|
|
|
// 标准模式使用默认的绝对定位,添加位置参数
|
|
containerDiv.style.right = '6px';
|
|
containerDiv.style.bottom = '8px';
|
|
|
|
// 直接添加到dom-widget容器,但不修改dom-widget本身
|
|
domWidgetContainer.appendChild(containerDiv);
|
|
|
|
// 触发回流,确保样式更新
|
|
void containerDiv.offsetWidth;
|
|
}
|
|
|
|
// 强制应用容器高度
|
|
containerDiv.style.height = '20px';
|
|
containerDiv.style.minHeight = '20px';
|
|
void containerDiv.offsetWidth; // 触发回流,确保样式应用
|
|
}
|
|
|
|
/**
|
|
* 清理单个实例的资源
|
|
*/
|
|
_cleanupInstance(instance, instanceKey, skipRemove = false) {
|
|
try {
|
|
if (!instance) 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) {
|
|
instance.cleanupListeners();
|
|
}
|
|
|
|
// 3. 清理所有保存的事件清理函数
|
|
if (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 && instance.element.parentNode) {
|
|
// 确保在移除元素前清理所有子元素的事件
|
|
const allButtons = instance.element.querySelectorAll('button');
|
|
allButtons.forEach(button => {
|
|
button.replaceWith(button.cloneNode(true));
|
|
});
|
|
instance.element.parentNode.removeChild(instance.element);
|
|
}
|
|
|
|
// 5. 清理输入框映射
|
|
if (window.PromptAssistantInputWidgetMap && instanceKey) {
|
|
delete window.PromptAssistantInputWidgetMap[instanceKey];
|
|
}
|
|
|
|
// 6. 清理弹窗状态
|
|
if (window.FEATURES && window.FEATURES.updateButtonsVisibility) {
|
|
try {
|
|
window.FEATURES.updateButtonsVisibility();
|
|
} catch (err) {
|
|
logger.debug(`按钮可见性更新 | 错误:${err.message}`);
|
|
}
|
|
}
|
|
|
|
// 7. 从实例集合中移除(除非明确指定跳过)
|
|
if (!skipRemove && instanceKey) {
|
|
PromptAssistant.instances.delete(instanceKey);
|
|
}
|
|
|
|
// 8. 清理实例属性
|
|
Object.keys(instance).forEach(key => {
|
|
try {
|
|
delete instance[key];
|
|
} catch (err) {
|
|
logger.debug(`属性清理 | 属性:${key} | 错误:${err.message}`);
|
|
}
|
|
});
|
|
|
|
logger.debug(`实例清理 | 结果:成功 | 实例:${instanceKey}`);
|
|
} catch (error) {
|
|
logger.error(`实例清理失败 | 实例:${instanceKey} | 错误:${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 创建单例实例
|
|
const promptAssistant = new PromptAssistant();
|
|
|
|
// 导出
|
|
export { promptAssistant, PromptAssistant }; |