Files
yawiii-ComfyUI-Prompt-Assis…/js/modules/imageCaption.js
T
2025-10-14 14:54:05 +08:00

2226 lines
82 KiB
JavaScript

/**
* 图像节点小助手类
* 用于检测和处理图像节点,提供图像反推功能
*/
import { app } from "../../../../scripts/app.js";
import { logger } from '../utils/logger.js';
import { ResourceManager } from "../utils/resourceManager.js";
import { UIToolkit } from "../utils/UIToolkit.js";
import { EventManager } from "../utils/eventManager.js";
import { APIService } from '../services/api.js';
import { HistoryCacheService } from '../services/cache.js';
import { buttonMenu } from "../services/btnMenu.js";
import { rulesConfigManager } from "./rulesConfigManager.js";
class ImageCaption {
/** 存储所有小助手实例的Map集合 */
static instances = new Map();
constructor() {
this.initialized = false;
}
/**
* 获取低画质渲染阈值
* 将此方法提取为类方法,确保所有地方使用相同的阈值计算逻辑
*/
_getQualityThreshold() {
// 优先从系统设置获取
if (app.ui?.settings) {
try {
// 修复:不使用已弃用的defaultValue参数
const settingValue = app.ui.settings.getSettingValue('Comfy.Graph.CanvasInfo');
if (typeof settingValue === 'number') {
return settingValue;
}
} catch (e) {
// 忽略错误,使用默认值
}
}
// 从canvas对象获取
return app.canvas?.low_quality_zoom_threshold || 0.6;
}
/**
* 检查节点类型是否是允许的图像节点类型
* @param {object} node - LiteGraph节点对象
* @param {boolean} debug - 是否打印调试日志
* @returns {boolean} 是否是允许的节点类型
*/
_isAllowedNodeType(node, debug = false) {
if (!node || !node.type) return false;
// 允许的节点类型列表(可根据实际情况调整)
const allowedTypes = [
// 加载图像节点
'LoadImage',
'LoadImageFromUrl',
// 预览图像节点
'PreviewImage',
'ImagePreview',
// 保存图像节点
'SaveImage',
'SaveImages'
];
// 检查节点类型是否在允许列表中
// 使用部分匹配方式,以便兼容不同插件中的类似节点
const isAllowed = allowedTypes.some(type =>
node.type.includes(type) ||
(node.title && node.title.includes(type))
);
// 开发阶段可打开调试日志
if (debug && !isAllowed) {
logger.debug(`[图像小助手] 节点类型不允许: ${node.type || '未知'} | 标题: ${node.title || '未知'}`);
}
return isAllowed;
}
/**
* 检查节点和画布状态是否适合显示小助手
* @param {object} node - LiteGraph节点对象
* @returns {object} 返回检测结果对象
*/
_checkNodeAndCanvasState(node) {
const result = {
isValid: false,
isCollapsed: false,
isLowQuality: false,
hasValidImage: false
};
if (!node) {
return result;
}
// 检查节点是否是允许的图像节点类型
if (!this._isAllowedNodeType(node)) {
return result;
}
// 检查节点是否被折叠
if (node.flags && node.flags.collapsed) {
result.isCollapsed = true;
return result;
}
// 检查是否处于低质量渲染状态
if (app.canvas) {
// 获取低画质渲染阈值
const threshold = this._getQualityThreshold();
const scale = app.canvas.ds.scale;
// 添加一个较大的容差值,解决浮点数比较和阈值应用延迟问题
const epsilon = 0.001;
if (scale <= threshold + epsilon) {
result.isLowQuality = true;
return result;
}
}
// 检查节点是否有有效图像
if (node.imgs && Array.isArray(node.imgs) && node.imgs.length > 0) {
const imageIndex = node.imageIndex || 0;
if (imageIndex >= 0 && imageIndex < node.imgs.length && node.imgs[imageIndex]) {
result.hasValidImage = true;
result.isValid = true;
}
}
return result;
}
/**
* 检查节点是否有有效图像
* @param {object} node - LiteGraph节点对象
* @returns {boolean} 是否有有效图像
*/
hasValidImage(node) {
if (!node) return false;
// 检查节点类型是否允许
if (!this._isAllowedNodeType(node)) {
return false;
}
// 检查节点是否有imgs属性
if (!node.imgs || !Array.isArray(node.imgs) || node.imgs.length === 0) {
return false;
}
// 获取当前显示的图像
const imageIndex = node.imageIndex || 0;
return imageIndex >= 0 && imageIndex < node.imgs.length && node.imgs[imageIndex] != null;
}
/**
* 获取节点当前显示的图像对象
* @param {object} node - LiteGraph节点对象
* @returns {object|null} 返回图像对象或null
*/
getNodeImage(node) {
// 检查节点类型是否允许
if (!this._isAllowedNodeType(node)) {
return null;
}
const state = this._checkNodeAndCanvasState(node);
if (!state.isValid) {
return null;
}
// 返回当前显示的图像
const imageIndex = node.imageIndex || 0;
return node.imgs[imageIndex];
}
/**
* 初始化图像小助手
*/
async initialize() {
if (this.initialized) return true;
try {
// 检查总开关的初始状态
const initialEnabled = app.ui.settings.getSettingValue("PromptAssistant.Features.ImageCaption");
window.FEATURES.imageCaption = initialEnabled !== undefined ? initialEnabled : true;
// 只在调试模式下记录初始化状态
logger.debug(`图像反推功能初始化 | 状态:${window.FEATURES.imageCaption ? "启用" : "禁用"}`);
// 注册节点选择监听器
this.registerNodeSelectionListener();
// 标记为已初始化
this.initialized = true;
logger.log("图像小助手初始化完成");
return true;
} catch (error) {
logger.error(`图像小助手初始化失败 | 错误: ${error.message}`);
this.initialized = false;
return false;
}
}
/**
* 注册节点选择事件监听
*/
registerNodeSelectionListener() {
if (!app.canvas) {
logger.error("画布未初始化,无法注册节点选择事件监听器");
return;
}
// 如果已经注册过选择事件处理器,则不再重复注册
if (app.canvas._imageCaptionSelectionHandler) {
logger.debug("图像小助手节点选择监听器已存在,跳过注册");
return;
}
// 创建选择事件处理器
const selectionHandler = (selected_nodes) => {
// 当总开关或图像反推功能关闭时,跳过所有节点处理
if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
return;
}
// 处理空选择情况
if (!selected_nodes || Object.keys(selected_nodes).length === 0) {
return;
}
// 处理选中的节点
for (const nodeId in selected_nodes) {
const node = app.canvas.graph._nodes_by_id[nodeId];
if (!node || node.id === -1) continue;
// 移除初始化标记判断,每次选中都重新检测节点状态
this.checkAndSetupNode(node);
}
};
// 保存原始的选择事件处理器
if (app.canvas.onSelectionChange && app.canvas.onSelectionChange !== selectionHandler) {
app.canvas._originalImageCaptionSelectionChange = app.canvas.onSelectionChange;
}
// 设置新的选择事件处理器
app.canvas._imageCaptionSelectionHandler = selectionHandler;
// 添加到LiteGraph的事件系统
if (app.canvas.graph) {
app.canvas.graph._imageCaptionNodeSelectionChange = selectionHandler;
}
logger.debug("图像小助手节点选择监听器注册成功");
// 初始检查当前选中的节点
if (app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length > 0) {
selectionHandler(app.canvas.selected_nodes);
}
}
/**
* 检查节点并设置小助手
*/
checkAndSetupNode(node) {
// 检查总开关和图像反推功能开关状态
if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
return;
}
// 检查节点是否有效
if (!node) return;
// 检查节点类型是否允许
if (!this._isAllowedNodeType(node)) {
return;
}
// 检查节点是否已被删除(只有在删除时才清理实例)
if (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[node.id]) {
// 节点已被删除,清理实例
if (ImageCaption.hasInstance(node.id)) {
this.cleanup(node.id);
}
return;
}
// 检查节点和画布状态
const nodeState = this._checkNodeAndCanvasState(node);
// 如果节点折叠或画布缩放过小,隐藏已存在的实例但不创建新实例
if (nodeState.isCollapsed || nodeState.isLowQuality) {
if (ImageCaption.hasInstance(node.id)) {
const instance = ImageCaption.getInstance(node.id);
if (instance) {
this.updateAssistantVisibility(instance);
}
}
return;
}
// 使用新的方法检查节点是否有有效且可见的图像
const currentImage = this.getNodeImage(node);
if (currentImage) {
// 有效图像,创建或显示小助手
const existingInstance = ImageCaption.getInstance(node.id);
// 验证现有实例是否有效
if (existingInstance && existingInstance.element && document.body.contains(existingInstance.element)) {
// 实例有效,显示它
this.showAssistantUI(existingInstance);
} else {
// 实例无效或不存在,清理并创建新实例
if (existingInstance) {
// 先清理可能存在但无效的实例
this.cleanup(node.id);
}
// 创建新的小助手实例
const assistant = this.setupNodeAssistant(node);
if (assistant) {
logger.log(`创建图像小助手 | ID: ${node.id}`);
}
}
}
}
/**
* 为节点设置小助手
*/
setupNodeAssistant(node) {
if (!node) return null;
// 创建小助手实例
const assistant = this.createAssistant(node);
if (assistant) {
// 初始化显示状态
this.showAssistantUI(assistant);
return assistant;
}
return null;
}
/**
* 创建小助手实例
*/
createAssistant(node) {
// 检查节点是否有效
if (!node || !node.id || node.id === -1) {
return null;
}
// 检查是否已存在实例
if (ImageCaption.hasInstance(node.id)) {
return ImageCaption.getInstance(node.id);
}
// 创建小助手对象
const assistant = {
node,
nodeId: node.id,
buttons: {},
isActive: false,
isTransitioning: false,
_eventCleanupFunctions: [], // 用于存储事件清理函数
_timers: {} // 用于存储定时器引用
};
// 创建UI
this.createAssistantUI(assistant);
// 添加到实例集合
ImageCaption.addInstance(node.id, assistant);
// 设置节点折叠状态监听
this._setupNodeCollapseListener(assistant);
// 设置画布缩放监听
this._setupCanvasScaleListener(assistant);
// 显示小助手
this.updateAssistantVisibility(assistant);
return assistant;
}
/**
* 创建小助手UI
*/
createAssistantUI(assistant) {
if (!assistant?.node) return null;
try {
// 创建内部内容容器
const innerContentDiv = document.createElement('div');
innerContentDiv.className = 'image-assistant-inner';
// 创建主容器
const containerDiv = document.createElement('div');
containerDiv.className = 'image-assistant-container image-assistant-transition';
containerDiv.dataset.nodeId = assistant.nodeId;
// 添加内容容器到主容器
containerDiv.appendChild(innerContentDiv);
// 创建悬停区域 - 用于检测鼠标悬停
const hoverAreaDiv = document.createElement('div');
hoverAreaDiv.className = 'image-assistant-hover-area';
// 由于初始状态为折叠,所以显示悬停区域
hoverAreaDiv.style.display = 'block';
containerDiv.appendChild(hoverAreaDiv);
// 创建折叠状态指示器图标
const indicatorDiv = document.createElement('div');
indicatorDiv.className = 'image-assistant-indicator animate-creation';
// 从ResourceManager获取图标并添加到指示器
const mainIcon = ResourceManager.getIcon('icon-main.svg');
if (mainIcon) {
indicatorDiv.appendChild(mainIcon);
}
containerDiv.appendChild(indicatorDiv);
// 保存引用
assistant.element = containerDiv;
assistant.innerContent = innerContentDiv;
assistant.hoverArea = hoverAreaDiv;
assistant.indicator = indicatorDiv;
assistant.buttons = {};
assistant.isCollapsed = true; // 初始状态为折叠
// 初始化UI组件和事件
this.addFunctionButtons(assistant);
// 默认隐藏状态
containerDiv.style.display = 'none';
// 使用固定定位样式
containerDiv.style.position = 'fixed';
containerDiv.style.zIndex = '1';
document.body.appendChild(containerDiv);
// 初始状态为折叠,添加折叠样式类
containerDiv.classList.add('collapsed');
// 延迟设置位置
requestAnimationFrame(() => {
this._setupUIPosition(assistant);
});
// 设置展开折叠事件
this._setupCollapseExpandEvents(assistant);
// 移除动画类的定时器
setTimeout(() => {
if (indicatorDiv && indicatorDiv.classList.contains('animate-creation')) {
indicatorDiv.classList.remove('animate-creation');
}
}, 1000);
return containerDiv;
} catch (error) {
logger.error(`图像小助手UI创建失败 | ID: ${assistant.nodeId} | ${error.message}`);
return null;
}
}
/**
* 添加功能按钮
*/
addFunctionButtons(assistant) {
if (!assistant?.element) return;
// 创建反推按钮(中文)
const buttonZh = this.addButtonWithIcon(assistant, {
id: 'caption_zh',
title: '反推提示词(中文)',
icon: 'icon-caption-zh',
onClick: async (e, assistant) => {
e.preventDefault();
e.stopPropagation();
await this.handleImageAnalysis(assistant, 'zh');
},
// 添加中文反推按钮的右键菜单
contextMenu: async (assistant) => {
let activePromptId = null;
let visionPrompts = [];
try {
const response = await fetch('/prompt_assistant/api/config/system_prompts');
if (response.ok) {
const data = await response.json();
activePromptId = data.active_prompts?.vision_zh || null;
if (data.vision_prompts) {
const originalOrder = Object.keys(data.vision_prompts);
originalOrder.forEach(key => {
if (key.startsWith('vision_zh')) {
const prompt = data.vision_prompts[key];
visionPrompts.push({
id: key,
name: prompt.name || key,
isActive: key === activePromptId
});
}
});
visionPrompts.sort((a, b) =>
originalOrder.indexOf(a.id) - originalOrder.indexOf(b.id)
);
}
}
} catch (error) {
logger.error(`获取中文反推提示词失败: ${error.message}`);
}
if (visionPrompts.length === 0) {
return [{ label: '未找到提示词', disabled: true }];
}
// 添加分割线和规则管理按钮
const menuItems = visionPrompts.map(prompt => ({
label: prompt.name,
icon: `<span class="pi ${prompt.isActive ? 'pi-check-circle active-status' : 'pi-circle-off inactive-status'}"></span>`,
onClick: async (context) => {
try {
const response = await fetch('/prompt_assistant/api/config/active_prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'vision_zh', prompt_id: prompt.id })
});
if (response.ok) {
UIToolkit.showStatusTip(context.buttonElement, 'success', `已切换到: ${prompt.name}`);
} else {
throw new Error(`服务器返回错误: ${response.status}`);
}
} catch (error) {
logger.error(`切换中文反推提示词失败: ${error.message}`);
UIToolkit.showStatusTip(context.buttonElement, 'error', `切换失败: ${error.message}`);
}
}
}));
menuItems.push({ type: 'separator' });
menuItems.push({
label: '规则管理',
icon: '<span class="pi pi-pen-to-square"></span>',
onClick: () => {
rulesConfigManager.showRulesConfigModal();
}
});
// 在规则管理下方添加 服务选择 子菜单(视觉模型提供商)
try {
const resp = await fetch('/prompt_assistant/api/config/vision');
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: `<span class=\"pi ${current === key ? 'pi-check-circle active-status' : 'pi-circle-off inactive-status'}\"></span>`,
onClick: async (context) => {
try {
const res = await fetch('/prompt_assistant/api/config/vision', {
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(`切换视觉模型提供商失败: ${err.message}`);
UIToolkit.showStatusTip(context.buttonElement, 'error', `切换失败: ${err.message}`);
}
}
}));
menuItems.push({
label: '服务选择',
icon: '<span class="pi pi-sparkles"></span>',
children
});
}
} catch (e) {
logger.error(`加载视觉模型提供商失败: ${e.message}`);
}
return menuItems;
}
});
// 创建反推按钮(英文)
const buttonEn = this.addButtonWithIcon(assistant, {
id: 'caption_en',
title: '反推提示词(英文)',
icon: 'icon-caption-en',
onClick: async (e, assistant) => {
e.preventDefault();
e.stopPropagation();
await this.handleImageAnalysis(assistant, 'en');
},
// 添加英文反推按钮的右键菜单
contextMenu: async (assistant) => {
let activePromptId = null;
let visionPrompts = [];
try {
const response = await fetch('/prompt_assistant/api/config/system_prompts');
if (response.ok) {
const data = await response.json();
activePromptId = data.active_prompts?.vision_en || null;
if (data.vision_prompts) {
const originalOrder = Object.keys(data.vision_prompts);
originalOrder.forEach(key => {
if (key.startsWith('vision_en')) {
const prompt = data.vision_prompts[key];
visionPrompts.push({
id: key,
name: prompt.name || key,
isActive: key === activePromptId
});
}
});
visionPrompts.sort((a, b) =>
originalOrder.indexOf(a.id) - originalOrder.indexOf(b.id)
);
}
}
} catch (error) {
logger.error(`获取英文反推提示词失败: ${error.message}`);
}
if (visionPrompts.length === 0) {
return [{ label: '未找到提示词', disabled: true }];
}
// 添加分割线和规则管理按钮
const menuItems = visionPrompts.map(prompt => ({
label: prompt.name,
icon: `<span class="pi ${prompt.isActive ? 'pi-check-circle active-status' : 'pi-circle-off inactive-status'}"></span>`,
onClick: async (context) => {
try {
const response = await fetch('/prompt_assistant/api/config/active_prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'vision_en', prompt_id: prompt.id })
});
if (response.ok) {
UIToolkit.showStatusTip(context.buttonElement, 'success', `已切换到: ${prompt.name}`);
} else {
throw new Error(`服务器返回错误: ${response.status}`);
}
} catch (error) {
logger.error(`切换英文反推提示词失败: ${error.message}`);
UIToolkit.showStatusTip(context.buttonElement, 'error', `切换失败: ${error.message}`);
}
}
}));
menuItems.push({ type: 'separator' });
menuItems.push({
label: '规则管理',
icon: '<span class="pi pi-pen-to-square"></span>',
onClick: () => {
rulesConfigManager.showRulesConfigModal();
}
});
// 在规则管理下方添加 服务选择 子菜单(视觉模型提供商)
try {
const resp = await fetch('/prompt_assistant/api/config/vision');
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: `<span class=\"pi ${current === key ? 'pi-check-circle active-status' : 'pi-circle-off inactive-status'}\"></span>`,
onClick: async (context) => {
try {
const res = await fetch('/prompt_assistant/api/config/vision', {
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(`切换视觉模型提供商失败: ${err.message}`);
UIToolkit.showStatusTip(context.buttonElement, 'error', `切换失败: ${err.message}`);
}
}
}));
menuItems.push({
label: '服务选择',
icon: '<span class="pi pi-sparkles"></span>',
children
});
}
} catch (e) {
logger.error(`加载视觉模型提供商失败: ${e.message}`);
}
return menuItems;
}
});
// 按顺序添加元素:中文按钮 -> 英文按钮
if (buttonZh) {
assistant.innerContent.appendChild(buttonZh);
}
if (buttonEn) {
assistant.innerContent.appendChild(buttonEn);
}
}
/**
* 处理图像分析
*/
async handleImageAnalysis(assistant, lang) {
// 存储当前请求ID,用于取消操作
let currentRequestId = null;
try {
const node = assistant.node;
if (!node || !node.imgs || node.imgs.length === 0) {
throw new Error('未找到有效的图像');
}
// 获取当前选中的图片
const currentImage = node.imgs[node.imageIndex || 0];
if (!currentImage) {
throw new Error('未找到有效的图像');
}
// 获取按钮元素
const buttonId = lang === 'en' ? 'caption_en' : 'caption_zh';
const buttonElement = assistant.buttons[buttonId];
if (!buttonElement) {
throw new Error('未找到按钮元素');
}
// 检查按钮是否已经处于处理状态,如果是,则取消当前请求
if (buttonElement.classList.contains('button-processing') && assistant.currentRequestId) {
// 取消当前请求
await APIService.cancelRequest(assistant.currentRequestId);
// 显示取消提示
UIToolkit.showStatusTip(
buttonElement,
'info',
'反推已取消',
{ x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
);
// 重置按钮状态
this._setButtonState(assistant, buttonId, 'processing', false);
// 恢复其他按钮状态
Object.keys(assistant.buttons).forEach(id => {
if (id !== buttonId) {
this._setButtonState(assistant, id, 'disabled', false);
}
});
// 更新小助手状态为非激活状态
this._updateAssistantActiveState(assistant, false);
// 清除当前请求ID
assistant.currentRequestId = null;
return;
}
// 设置当前按钮为处理中状态
this._setButtonState(assistant, buttonId, 'processing', true);
// 禁用其他按钮
Object.keys(assistant.buttons).forEach(id => {
if (id !== buttonId) {
this._setButtonState(assistant, id, 'disabled', true);
}
});
// 更新小助手状态为激活状态
this._updateAssistantActiveState(assistant, true);
// 显示加载状态提示
const tipMessage = lang === 'en' ? "反推提示词...(英文)" : "反推提示词...(中文)";
UIToolkit.showStatusTip(
buttonElement,
'loading',
tipMessage,
{ x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
);
// 生成请求ID
currentRequestId = `${node.id}_${Date.now()}`;
// 保存到assistant对象中,以便取消操作
assistant.currentRequestId = currentRequestId;
// 将图像转换为Base64
let imageBase64;
try {
imageBase64 = await APIService.imageToBase64(currentImage);
if (!imageBase64) {
throw new Error('图像转换失败');
}
} catch (e) {
throw new Error(`图像转换失败: ${e.message || e}`);
}
// 确保图像数据格式正确
if (typeof imageBase64 !== 'string') {
throw new Error(`图像数据类型错误: ${typeof imageBase64}`);
}
// 确保图像数据是Base64格式
if (!imageBase64.startsWith('data:image')) {
imageBase64 = `data:image/jpeg;base64,${imageBase64}`;
}
// 获取当前激活的提示词内容
let promptContent = '';
try {
const response = await fetch('/prompt_assistant/api/config/system_prompts');
if (response.ok) {
const data = await response.json();
const activePromptKey = `vision_${lang}`;
const activePromptId = data.active_prompts?.[activePromptKey];
if (activePromptId && data.vision_prompts?.[activePromptId]) {
promptContent = data.vision_prompts[activePromptId].content;
}
}
if (!promptContent) {
throw new Error(`未找到有效的 ${lang === 'zh' ? '中文' : '英文'} 反推提示词`);
}
} catch (error) {
throw new Error(`获取反推规则失败: ${error.message}`);
}
// 调用图像分析服务,传入提示词内容
const result = await APIService.llmAnalyzeImage(imageBase64, promptContent, currentRequestId);
// 清除当前请求ID
assistant.currentRequestId = null;
// 检查是否被取消
if (result && result.cancelled) {
logger.debug(`图像分析请求已取消 | ID: ${currentRequestId}`);
return;
}
if (!result || !result.success) {
const errorMsg = result?.error || '未知错误';
throw new Error(errorMsg);
}
// 获取描述文本
const description = result.data.description;
if (!description) {
throw new Error('未获取到图像描述');
}
// 尝试复制到剪贴板
let copySuccess = false;
try {
// 优先使用现代的 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(description);
copySuccess = true;
} else {
// 创建一个临时的textarea元素
const textarea = document.createElement('textarea');
textarea.value = description;
textarea.style.position = 'fixed';
textarea.style.left = '0';
textarea.style.top = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
// 尝试聚焦并选中文本
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
// 尝试复制
try {
copySuccess = document.execCommand('copy');
} catch (err) {
logger.warn(`execCommand复制失败: ${err.message}`);
}
// 移除临时元素
document.body.removeChild(textarea);
}
if (!copySuccess) {
throw new Error('复制到剪贴板操作未能成功执行');
}
} catch (copyError) {
logger.warn(`复制到剪贴板失败: ${copyError.message}`);
// 即使复制失败也继续执行,不抛出错误,但记录状态
copySuccess = false;
}
// 为当前图像节点创建历史记录
HistoryCacheService.addHistory({
node_id: node.id,
input_id: "image",
content: description,
operation_type: 'caption',
timestamp: Date.now(),
request_id: currentRequestId
});
// 显示成功提示
const successMessage = copySuccess
? (lang === 'en' ? "反推完成,已复制到剪贴板" : "反推完成,已复制到剪贴板")
: (lang === 'en' ? "反推完成,但复制失败" : "反推完成,但复制失败");
UIToolkit.showStatusTip(
buttonElement,
copySuccess ? 'success' : 'warning',
successMessage,
{ x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
);
// 限制Toast显示的文本长度
const maxLength = 40;
const truncatedDescription = description.length > maxLength
? description.substring(0, maxLength) + '...'
: description;
// 显示Toast提示
app.extensionManager.toast.add({
severity: copySuccess ? "success" : "info",
summary: copySuccess
? (lang === 'en' ? "图像反推完成(英文),请使用 ctrl+v 粘贴" : "图像反推完成(中文),请使用 ctrl+v 粘贴")
: (lang === 'en' ? "图像反推完成(英文),但复制失败,请手动复制" : "图像反推完成(中文),但复制失败,请手动复制"),
detail: truncatedDescription,
life: 5000
});
// 如果复制失败,创建一个对话框显示结果,允许用户手动复制
if (!copySuccess) {
this._showCopyDialog(description, lang);
}
} catch (error) {
// 清除当前请求ID
assistant.currentRequestId = null;
logger.error(`图像分析失败: ${error.message}`);
// 获取按钮元素
const buttonId = lang === 'en' ? 'caption_en' : 'caption_zh';
const buttonElement = assistant.buttons[buttonId];
if (buttonElement) {
// 显示错误提示
UIToolkit.showStatusTip(
buttonElement,
'error',
error.message,
{ x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
);
};
app.extensionManager.toast.add({
severity: "error",
summary: lang === 'en' ? "Error" : "错误",
detail: error.message,
life: 3000
});
} finally {
// 获取按钮ID
const buttonId = lang === 'en' ? 'caption_en' : 'caption_zh';
// 重置当前按钮状态
this._setButtonState(assistant, buttonId, 'processing', false);
// 恢复其他按钮状态
Object.keys(assistant.buttons).forEach(id => {
if (id !== buttonId) {
this._setButtonState(assistant, id, 'disabled', false);
}
});
// 更新小助手状态为非激活状态
this._updateAssistantActiveState(assistant, false);
}
}
/**
* 显示复制对话框
* 当剪贴板API失败时,提供一个对话框让用户手动复制内容
*/
_showCopyDialog(content, lang) {
// 创建对话框容器
const dialogContainer = document.createElement('div');
dialogContainer.className = 'image-assistant-copy-dialog';
// 创建标题
const title = document.createElement('div');
title.className = 'image-assistant-copy-dialog-title';
title.textContent = '由于剪贴板权限被限制,请手动点击复制提示词内容';
dialogContainer.appendChild(title);
// 创建关闭按钮
const closeButton = document.createElement('button');
closeButton.className = 'image-assistant-copy-dialog-close';
closeButton.onclick = () => {
document.body.removeChild(dialogContainer);
};
// 添加关闭图标
UIToolkit.addIconToButton(closeButton, 'pi-times', '关闭');
dialogContainer.appendChild(closeButton);
// 创建文本区域
const contentArea = document.createElement('textarea');
contentArea.className = 'image-assistant-copy-dialog-textarea';
contentArea.value = content;
contentArea.readOnly = true;
dialogContainer.appendChild(contentArea);
// 创建复制按钮
const copyButton = document.createElement('button');
copyButton.className = 'image-assistant-copy-dialog-copy-btn';
copyButton.textContent = '复制到剪贴板';
copyButton.onclick = () => {
contentArea.select();
try {
const success = document.execCommand('copy');
if (success) {
copyButton.textContent = '复制成功!';
// 复制成功后,延迟500ms关闭弹窗,让用户看到成功提示
setTimeout(() => {
if (document.body.contains(dialogContainer)) {
document.body.removeChild(dialogContainer);
}
}, 500);
} else {
copyButton.textContent = '复制失败,请手动选择和复制';
contentArea.focus();
}
} catch (err) {
copyButton.textContent = '复制失败,请手动选择和复制';
contentArea.focus();
}
};
dialogContainer.appendChild(copyButton);
// 添加到文档
document.body.appendChild(dialogContainer);
// 聚焦内容区域,使用户可以立即复制
setTimeout(() => {
contentArea.focus();
contentArea.select();
}, 100);
}
/**
* 设置按钮状态
*/
_setButtonState(assistant, buttonId, stateType, value = true) {
try {
const button = assistant.buttons[buttonId];
if (!button) return;
const stateClass = `button-${stateType}`;
if (value) {
button.classList.add(stateClass);
// 如果是禁用状态,添加disabled属性
if (stateType === 'disabled') {
button.setAttribute('disabled', 'disabled');
}
} else {
button.classList.remove(stateClass);
// 如果取消禁用状态,移除disabled属性
if (stateType === 'disabled') {
button.removeAttribute('disabled');
}
}
// 更新按钮可点击状态
this._updateButtonClickability(button, stateType, value);
} catch (error) {
logger.error(`按钮状态 | 设置失败 | 按钮:${buttonId} | 状态:${stateType} | 错误:${error.message}`);
}
}
/**
* 更新按钮可点击状态
*/
_updateButtonClickability(button, stateType, value) {
// 检查按钮是否处于禁用状态
const isDisabled = button.classList.contains('button-disabled');
// 处理中的按钮仍然可点击(用于取消操作)
const isProcessing = button.classList.contains('button-processing');
if (isDisabled) {
// 如果按钮被禁用,阻止点击事件
button.style.pointerEvents = 'none';
} else {
// 恢复点击事件,包括处理中的按钮
button.style.pointerEvents = 'auto';
}
}
/**
* 检查小助手是否有按钮处于激活状态
*/
_checkAssistantActiveState(assistant) {
if (!assistant || !assistant.buttons) return false;
// 遍历所有按钮,检查是否有按钮处于active或processing状态
for (const buttonId in assistant.buttons) {
const button = assistant.buttons[buttonId];
if (button.classList.contains('button-active') ||
button.classList.contains('button-processing')) {
return true;
}
}
return false;
}
/**
* 更新小助手激活状态
*/
_updateAssistantActiveState(assistant, isActive) {
if (!assistant) return;
// 更新激活状态
assistant.isActive = isActive;
// 如果激活,强制显示小助手
if (isActive) {
this.showAssistantUI(assistant);
} else {
// 如果不再激活,先更新可见性
this.updateAssistantVisibility(assistant);
// 然后手动触发自动折叠(如果小助手仍然可见且处于展开状态)
if (assistant.element &&
assistant.element.style.display !== 'none' &&
!assistant.isCollapsed &&
!assistant.isTransitioning) {
// 延迟一点时间再触发折叠,让用户有时间看到结果
setTimeout(() => {
this.triggerAutoCollapse(assistant);
}, 1500); // 1.5秒后自动折叠,给用户足够时间查看结果
}
}
}
/**
* 显示小助手UI
*/
showAssistantUI(assistant) {
if (!assistant?.element) return;
// 避免重复显示
if (assistant.element.classList.contains('image-assistant-show')) {
// 确保元素可见
assistant.element.style.display = 'flex';
assistant.element.style.opacity = '1';
return;
}
// 直接显示,无动画过渡
assistant.element.style.opacity = '1';
assistant.element.style.display = 'flex';
assistant.element.classList.add('image-assistant-show');
// 确保悬停区域可见(用于折叠状态下的交互)
if (assistant.isCollapsed && assistant.hoverArea) {
assistant.hoverArea.style.display = 'block';
}
// 重置过渡状态
assistant.isTransitioning = false;
// 只有当明确不是折叠状态时才触发自动折叠
if (!assistant.isCollapsed) {
this.triggerAutoCollapse(assistant);
}
}
/**
* 隐藏小助手UI
*/
hideAssistantUI(assistant) {
if (!assistant?.element) return;
// 清除自动折叠定时器
if (assistant._autoCollapseTimer) {
clearTimeout(assistant._autoCollapseTimer);
assistant._autoCollapseTimer = null;
}
// 隐藏元素
assistant.element.style.display = 'none';
assistant.element.classList.remove('image-assistant-show');
// 重置状态
assistant.isTransitioning = false;
}
/**
* 更新小助手可见性
*/
updateAssistantVisibility(assistant) {
if (!assistant) return;
// 记录当前显示状态,用于检测变化
const wasVisible = assistant.element &&
assistant.element.style.display !== 'none' &&
assistant.element.classList.contains('image-assistant-show');
// 检查总开关和图像反推功能开关状态
if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
this.cleanup(assistant.node.id);
return;
}
// 检查节点是否已被删除(关键修复)
if (assistant.node && (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[assistant.node.id])) {
// 清理已删除节点的实例
this.cleanup(assistant.node.id);
return;
}
// 使用统一的状态检测方法
const nodeState = this._checkNodeAndCanvasState(assistant.node);
// 检查是否有按钮处于激活状态
const hasActiveButtons = this._checkAssistantActiveState(assistant);
// 确定新的可见性状态
let shouldBeVisible = true;
// 如果有激活的按钮,强制显示小助手(覆盖其他隐藏条件)
if (hasActiveButtons) {
this.showAssistantUI(assistant);
// 如果当前是折叠状态,则展开
if (assistant.isCollapsed) {
this._expandAssistant(assistant);
}
return;
}
// 如果节点折叠或画布缩放过小,隐藏小助手但不清理实例
if (nodeState.isCollapsed || nodeState.isLowQuality) {
shouldBeVisible = false;
}
// 如果节点没有有效图像,隐藏小助手但不清理实例
if (!nodeState.hasValidImage) {
shouldBeVisible = false;
}
// 跳过正在过渡的实例,避免动画中断
if (assistant.isTransitioning) {
return;
}
// 根据可见性状态更新UI
if (shouldBeVisible) {
// 条件满足时显示小助手
this.showAssistantUI(assistant);
} else {
// 隐藏小助手
this.hideAssistantUI(assistant);
}
}
/**
* 展开小助手
*/
_expandAssistant(assistant) {
if (!assistant || !assistant.element || !assistant.isCollapsed || assistant.isTransitioning) return;
assistant.isTransitioning = true;
// 隐藏悬停区域,避免覆盖按钮
if (assistant.hoverArea) {
assistant.hoverArea.style.display = 'none';
}
const containerDiv = assistant.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 = 'fixed';
// 强制回流并测量
void containerDiv.offsetWidth;
// 获取自然宽度
const naturalWidth = containerDiv.offsetWidth;
targetWidth = naturalWidth + 'px';
// 保存宽度供将来使用
containerDiv.style.setProperty('--expanded-width', targetWidth);
// 恢复原始样式
containerDiv.style.opacity = originalOpacity;
containerDiv.style.position = 'fixed';
// 重新应用折叠类以准备动画
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 = '';
assistant.isCollapsed = false;
assistant.isTransitioning = false;
}, 300);
}
/**
* 触发自动折叠
*/
triggerAutoCollapse(assistant) {
// 如果widget没有初始化或者已经处于折叠状态,则不处理
if (!assistant || !assistant.element || assistant.isCollapsed || assistant.isTransitioning) return;
// 如果有活跃按钮,不自动折叠
if (this._checkAssistantActiveState(assistant)) return;
// 如果鼠标当前悬停在容器上,不自动折叠
if (assistant.isMouseOver) return;
// 清除可能已存在的自动折叠定时器
if (assistant._autoCollapseTimer) {
clearTimeout(assistant._autoCollapseTimer);
}
// 设置自动折叠定时器
assistant._autoCollapseTimer = setTimeout(() => {
// 再次检查条件
if (!assistant.isCollapsed && !assistant.isTransitioning &&
!this._checkAssistantActiveState(assistant) && !assistant.isMouseOver) {
const containerDiv = assistant.element;
// 保存当前宽度用于展开动画
if (containerDiv.offsetWidth > 0) {
containerDiv.style.setProperty('--expanded-width', `${containerDiv.offsetWidth}px`);
}
// 设置过渡状态
assistant.isTransitioning = true;
// 直接添加折叠类
containerDiv.classList.add('collapsed');
assistant.isCollapsed = true;
// 显示悬停区域,用于检测鼠标悬停以展开UI
if (assistant.hoverArea) {
assistant.hoverArea.style.display = 'block';
}
// 动画结束后重置过渡状态
setTimeout(() => {
assistant.isTransitioning = false;
}, 300);
}
assistant._autoCollapseTimer = null;
}, 1000);
}
/**
* 添加带图标的按钮
*/
addButtonWithIcon(assistant, config) {
if (!assistant?.element || !assistant?.innerContent) return null;
const { id, title, icon, onClick, contextMenu } = config;
// 创建按钮
const button = document.createElement('button');
button.className = 'image-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, assistant);
});
}
// 添加右键菜单(如果有)
if (contextMenu && typeof contextMenu === 'function') {
this._setupButtonContextMenu(button, contextMenu, assistant);
}
// 保存引用
if (id) {
assistant.buttons[id] = button;
}
return button;
}
/**
* 设置UI位置
*/
_setupUIPosition(assistant) {
if (!assistant?.element || !assistant?.node) return;
const containerDiv = assistant.element;
// 更新位置的函数
const updatePosition = () => {
if (!assistant.element || !assistant.node) return;
try {
const canvas = app.canvas;
// 如果canvas未初始化,延迟重试
if (!canvas) {
requestAnimationFrame(() => updatePosition());
return;
}
// 获取画布缩放比例
const scale = canvas.ds.scale;
// 获取节点边界
const [nodeX, nodeY, nodeWidth, nodeHeight] = assistant.node.getBounding();
// 计算内部偏移量(用于将小助手放在节点内部)
const INNER_OFFSET_X = 6; // 水平偏移量
const INNER_OFFSET_Y = 6; // 垂直偏移量
// 计算定位点位置(节点左下角)
const anchorX = nodeX + INNER_OFFSET_X;
const anchorY = nodeY + nodeHeight - INNER_OFFSET_Y;
// 获取画布元素的边界
const rect = canvas.canvas.getBoundingClientRect();
// 将定位点位置转换为屏幕坐标
const canvasPoint = canvas.convertOffsetToCanvas([anchorX, anchorY]);
if (!canvasPoint) return;
// 计算最终的屏幕坐标(考虑画布元素的位置)
const screenX = canvasPoint[0] + rect.left;
const screenY = canvasPoint[1] + rect.top;
// 设置容器位置,使其左下角与定位点对齐
containerDiv.style.left = `${screenX}px`;
containerDiv.style.bottom = `${window.innerHeight - screenY}px`;
containerDiv.style.right = 'auto';
containerDiv.style.top = 'auto';
// 应用缩放
containerDiv.style.setProperty('--assistant-scale', scale);
} catch (error) {
logger.error(`更新小助手位置失败: ${error.message}`);
}
};
// 初始更新位置
updatePosition();
// 使用防抖函数优化位置更新
const debouncedUpdatePosition = EventManager.debounce(updatePosition, 16);
// 添加窗口resize事件监听
assistant._eventCleanupFunctions = assistant._eventCleanupFunctions || [];
const removeResizeListener = EventManager.addDOMListener(window, 'resize', debouncedUpdatePosition);
assistant._eventCleanupFunctions.push(removeResizeListener);
// 监听画布变化
if (app.canvas) {
// 监听画布重绘
const originalDrawBackground = app.canvas.onDrawBackground;
app.canvas.onDrawBackground = function () {
const ret = originalDrawBackground?.apply(this, arguments);
// 直接调用updatePosition确保位置准确
updatePosition();
return ret;
};
// 添加画布重绘清理函数
assistant._eventCleanupFunctions.push(() => {
if (originalDrawBackground) {
app.canvas.onDrawBackground = originalDrawBackground;
}
});
// 监听节点移动
if (assistant.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 === assistant.node.id) {
updatePosition();
}
};
// 添加节点移动清理函数
assistant._eventCleanupFunctions.push(() => {
if (app.canvas) {
app.canvas.onNodeMoved = originalOnNodeMoved;
}
});
// 为节点本身添加移动监听(兼容性处理)
const nodeOriginalOnNodeMoved = assistant.node.onNodeMoved;
assistant.node.onNodeMoved = function () {
const ret = nodeOriginalOnNodeMoved?.apply(this, arguments);
updatePosition();
return ret;
};
// 添加节点自身移动清理函数
assistant._eventCleanupFunctions.push(() => {
if (assistant.node && nodeOriginalOnNodeMoved) {
assistant.node.onNodeMoved = nodeOriginalOnNodeMoved;
}
});
}
// 监听画布缩放
const originalDSModified = app.canvas.ds.onModified;
app.canvas.ds.onModified = function (...args) {
if (originalDSModified) {
originalDSModified.apply(this, args);
}
// 缩放时直接更新
updatePosition();
};
// 添加画布缩放清理函数
assistant._eventCleanupFunctions.push(() => {
if (app.canvas?.ds) {
app.canvas.ds.onModified = originalDSModified;
}
});
}
// 添加DOM元素清理函数
assistant._eventCleanupFunctions.push(() => {
if (containerDiv && document.body.contains(containerDiv)) {
document.body.removeChild(containerDiv);
}
});
}
/**
* 设置节点折叠状态监听
*/
_setupNodeCollapseListener(assistant) {
if (!assistant?.node) return;
const node = assistant.node;
// 保存原始的 collapse 方法
const originalCollapse = node.collapse;
// 重写 collapse 方法
node.collapse = function () {
// 调用原始的 collapse 方法
originalCollapse.apply(this, arguments);
// 调用统一的可见性更新方法,而不是直接操作DOM
const imageCaptionInstance = window.imageCaptionInstance || imageCaption;
if (imageCaptionInstance && typeof imageCaptionInstance.updateAssistantVisibility === 'function') {
imageCaptionInstance.updateAssistantVisibility(assistant);
}
};
// 保存清理函数
assistant._eventCleanupFunctions.push(() => {
// 恢复原始的 collapse 方法
if (node.collapse !== originalCollapse) {
node.collapse = originalCollapse;
}
});
}
/**
* 设置画布缩放监听
*/
_setupCanvasScaleListener(assistant) {
if (!assistant?.node || !app.canvas) return;
let lastScale = app.canvas.ds.scale;
// 使用类方法获取阈值,确保一致性
const threshold = this._getQualityThreshold();
// 直接检测缩放状态并更新UI可见性
const checkScaleAndUpdate = () => {
if (!assistant.element || !assistant.node || !app.canvas) return;
const currentScale = app.canvas.ds.scale;
const threshold = imageCaption._getQualityThreshold(); // 修复:使用全局实例的方法
const epsilon = 0.001; // 增加容差值
// 计算与上次缩放的差值
const scaleDiff = Math.abs(currentScale - lastScale);
// 更新上次缩放值
lastScale = currentScale;
// 判断当前画质状态(使用容差值)
const isCurrentlyLowQuality = currentScale <= threshold + epsilon;
// 无条件更新UI可见性,让_checkNodeAndCanvasState方法决定是否显示
this.updateAssistantVisibility(assistant);
};
// 确保立即响应
const immediateUpdate = checkScaleAndUpdate;
// 监听画布的鼠标滚轮事件(缩放)
const wheelHandler = (e) => {
if (e.ctrlKey || e.metaKey) {
// 立即更新并安排延迟检查
immediateUpdate();
setTimeout(immediateUpdate, 50);
}
};
// 监听画布的触摸事件(移动端缩放)
const touchHandler = (e) => {
if (e.touches && e.touches.length === 2) {
// 立即更新并安排延迟检查
immediateUpdate();
setTimeout(immediateUpdate, 50);
}
};
// 添加事件监听器
const canvas = app.canvas.canvas;
if (canvas) {
canvas.addEventListener('wheel', wheelHandler, { passive: true });
canvas.addEventListener('touchmove', touchHandler, { passive: true });
}
// 定期检查缩放变化
const scaleCheckInterval = setInterval(immediateUpdate, 50);
// 初始检查一次当前状态
immediateUpdate();
// 保存清理函数
assistant._eventCleanupFunctions.push(() => {
if (canvas) {
canvas.removeEventListener('wheel', wheelHandler);
canvas.removeEventListener('touchmove', touchHandler);
}
clearInterval(scaleCheckInterval);
});
// 监听画布缩放事件(直接监听ds.scale变化)
const originalDSScale = Object.getOwnPropertyDescriptor(app.canvas.ds, 'scale');
if (originalDSScale && originalDSScale.set) {
const originalSetter = originalDSScale.set;
Object.defineProperty(app.canvas.ds, 'scale', {
get: originalDSScale.get,
set: function (value) {
// 获取旧值
const oldValue = this.scale;
const threshold = imageCaption._getQualityThreshold();
// 调用原始setter
originalSetter.call(this, value);
// 检测是否跨越了阈值边界
const epsilon = 0.001;
const crossedThreshold =
(oldValue <= threshold + epsilon && value > threshold + epsilon) ||
(oldValue > threshold + epsilon && value <= threshold + epsilon);
if (crossedThreshold) {
// 如果跨越了阈值,只记录一次日志
// 使用静态变量记录上次跨越阈值的时间,避免短时间内重复输出
const now = Date.now();
if (!ImageCaption._lastThresholdCrossTime || now - ImageCaption._lastThresholdCrossTime > 500) {
logger.log(`[图像小助手-缩放监听] 跨越阈值 | 旧值: ${oldValue.toFixed(4)} | 新值: ${value.toFixed(4)} | 阈值: ${threshold}`);
ImageCaption._lastThresholdCrossTime = now;
}
}
// 无论是否跨越阈值,都立即更新
immediateUpdate();
// 多次检查确保状态正确应用
setTimeout(immediateUpdate, 10);
setTimeout(immediateUpdate, 50);
setTimeout(() => {
// 更新所有已存在的图像小助手实例
ImageCaption.instances.forEach((instance) => {
if (instance && instance.node) {
imageCaption.updateAssistantVisibility(instance);
}
});
}, 100);
},
configurable: true
});
// 添加清理函数
assistant._eventCleanupFunctions.push(() => {
if (app.canvas && app.canvas.ds) {
Object.defineProperty(app.canvas.ds, 'scale', originalDSScale);
}
});
}
}
/**
* 设置展开折叠事件
*/
_setupCollapseExpandEvents(assistant) {
if (!assistant?.element) return;
const containerDiv = assistant.element;
// 记录原始宽度,用于展开动画
const saveOriginalWidth = () => {
if (!assistant.isCollapsed && containerDiv.offsetWidth > 0) {
containerDiv.style.setProperty('--expanded-width', `${containerDiv.offsetWidth}px`);
}
};
// 延迟保存宽度,确保DOM已完全渲染
setTimeout(saveOriginalWidth, 300);
// 折叠函数
const collapseAssistant = () => {
if (assistant.isCollapsed || assistant.isTransitioning) return;
// 保存当前宽度用于展开动画
saveOriginalWidth();
assistant.isTransitioning = true;
// 直接添加折叠类
containerDiv.classList.add('collapsed');
assistant.isCollapsed = true;
// 显示悬停区域,用于检测鼠标悬停以展开UI
assistant.hoverArea.style.display = 'block';
// 动画结束后重置过渡状态
setTimeout(() => {
assistant.isTransitioning = false;
}, 300);
};
// 展开函数
const expandAssistant = () => {
if (!assistant.isCollapsed || assistant.isTransitioning) return;
assistant.isTransitioning = true;
// 隐藏悬停区域,避免覆盖按钮
assistant.hoverArea.style.display = 'none';
// 计算展开后的宽度
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 = 'fixed';
// 强制回流并测量
void containerDiv.offsetWidth;
// 获取自然宽度
const naturalWidth = containerDiv.offsetWidth;
targetWidth = naturalWidth + 'px';
// 保存宽度供将来使用
containerDiv.style.setProperty('--expanded-width', targetWidth);
// 恢复原始样式
containerDiv.style.opacity = originalOpacity;
containerDiv.style.position = 'fixed';
// 重新应用折叠类以准备动画
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 = '';
assistant.isCollapsed = false;
assistant.isTransitioning = false;
}, 300);
};
// 创建折叠定时器变量
let collapseTimer = null;
let autoCollapseTimer = null;
// 鼠标离开容器时折叠
const handleMouseLeave = () => {
// 如果有活跃按钮,不折叠
if (this._checkAssistantActiveState(assistant)) return;
// 设置延时,避免鼠标短暂离开就触发折叠
collapseTimer = setTimeout(() => {
collapseAssistant();
}, 500);
};
// 鼠标进入容器时取消折叠定时器
const handleMouseEnter = () => {
if (collapseTimer) {
clearTimeout(collapseTimer);
collapseTimer = null;
}
// 取消自动折叠定时器
if (autoCollapseTimer) {
clearTimeout(autoCollapseTimer);
autoCollapseTimer = null;
}
// 如果当前是折叠状态,则展开
if (assistant.isCollapsed) {
expandAssistant();
}
};
// 为容器添加鼠标事件
const removeContainerMouseLeave = EventManager.addDOMListener(containerDiv, 'mouseleave', handleMouseLeave);
const removeContainerMouseEnter = EventManager.addDOMListener(containerDiv, 'mouseenter', handleMouseEnter);
// 为悬停区域添加鼠标事件
const removeHoverAreaMouseEnter = EventManager.addDOMListener(assistant.hoverArea, 'mouseenter', handleMouseEnter);
// 添加清理函数
assistant._eventCleanupFunctions = assistant._eventCleanupFunctions || [];
assistant._eventCleanupFunctions.push(removeContainerMouseLeave);
assistant._eventCleanupFunctions.push(removeContainerMouseEnter);
assistant._eventCleanupFunctions.push(removeHoverAreaMouseEnter);
// 添加清理定时器的函数
assistant._eventCleanupFunctions.push(() => {
if (collapseTimer) {
clearTimeout(collapseTimer);
collapseTimer = null;
}
if (autoCollapseTimer) {
clearTimeout(autoCollapseTimer);
autoCollapseTimer = null;
}
});
// 创建后自动折叠功能
const setupAutoCollapse = () => {
// 如果有活跃按钮,不自动折叠
if (this._checkAssistantActiveState(assistant)) return;
// 设置自动折叠定时器,1秒后自动折叠
autoCollapseTimer = setTimeout(() => {
// 再次检查是否有活跃按钮或鼠标悬停在容器上
if (!this._checkAssistantActiveState(assistant) && !assistant.isMouseOver) {
collapseAssistant();
}
}, 1000);
};
// 添加鼠标悬停状态跟踪
assistant.isMouseOver = false;
const trackMouseOver = () => {
assistant.isMouseOver = true;
};
const trackMouseOut = () => {
assistant.isMouseOver = false;
};
// 为容器和悬停区域添加鼠标悬停状态跟踪
const removeContainerMouseOverTracking = EventManager.addDOMListener(containerDiv, 'mouseover', trackMouseOver);
const removeContainerMouseOutTracking = EventManager.addDOMListener(containerDiv, 'mouseout', trackMouseOut);
const removeHoverAreaMouseOverTracking = EventManager.addDOMListener(assistant.hoverArea, 'mouseover', trackMouseOver);
const removeHoverAreaMouseOutTracking = EventManager.addDOMListener(assistant.hoverArea, 'mouseout', trackMouseOut);
// 添加清理函数
assistant._eventCleanupFunctions.push(removeContainerMouseOverTracking);
assistant._eventCleanupFunctions.push(removeContainerMouseOutTracking);
assistant._eventCleanupFunctions.push(removeHoverAreaMouseOverTracking);
assistant._eventCleanupFunctions.push(removeHoverAreaMouseOutTracking);
// 设置自动折叠(延迟执行,确保DOM已完全渲染)
setTimeout(setupAutoCollapse, 500);
}
/**
* 设置按钮右键菜单
* @param {HTMLElement} button 按钮元素
* @param {Function} getMenuItems 获取菜单项的函数
* @param {Object} assistant 小助手实例
*/
_setupButtonContextMenu(button, getMenuItems, assistant) {
if (!button || typeof getMenuItems !== 'function') return;
// 确保assistant对象具有正确的类型标识,方便右键菜单关闭时识别
assistant.type = 'image_caption_assistant';
const cleanup = buttonMenu.setupButtonMenu(button, () => {
return getMenuItems(assistant);
}, { widget: assistant, buttonElement: button }); // 传递正确的上下文
if (cleanup) {
assistant._eventCleanupFunctions = assistant._eventCleanupFunctions || [];
assistant._eventCleanupFunctions.push(cleanup);
}
}
// ---静态方法---
/**
* 添加实例到管理器
*/
static addInstance(nodeId, assistant) {
if (nodeId != null && assistant != null) {
this.instances.set(String(nodeId), assistant);
return true;
}
return false;
}
/**
* 获取实例
*/
static getInstance(nodeId) {
if (nodeId == null) return null;
// 确保nodeId是字符串类型
const key = String(nodeId);
const instance = this.instances.get(key);
return instance;
}
/**
* 检查实例是否存在
*/
static hasInstance(nodeId) {
if (nodeId == null) return false;
// 确保nodeId是字符串类型
return this.instances.has(String(nodeId));
}
/**
* 清理小助手实例
* @param {string|null} nodeId - 节点ID,如果为null则清理所有实例
* @param {boolean} silent - 是否静默清理(不输出日志)
*/
cleanup(nodeId = null, silent = false) {
// 如果正在切换工作流,完全清理图像小助手实例
if (window.PROMPT_ASSISTANT_WORKFLOW_SWITCHING) {
// 简化日志:工作流切换期间不逐条打印节点清理日志
if (nodeId === null) {
// 清理所有实例,并从集合中移除
const instanceCount = ImageCaption.instances.size;
if (instanceCount > 0) {
ImageCaption.instances.forEach((assistant, id) => {
this._cleanupSingleInstance(assistant);
});
// 清空实例集合
ImageCaption.instances.clear();
}
} else {
// 清理特定节点实例
const assistant = ImageCaption.getInstance(nodeId);
if (assistant) {
this._cleanupSingleInstance(assistant);
ImageCaption.instances.delete(String(nodeId));
}
}
return;
}
try {
if (nodeId === null) {
// 清理所有实例
const instanceCount = ImageCaption.instances.size;
if (instanceCount > 0) {
ImageCaption.instances.forEach((assistant, id) => {
this._cleanupSingleInstance(assistant);
});
ImageCaption.instances.clear();
if (!silent) {
logger.log(`清理所有图像小助手实例 | 数量: ${instanceCount}`);
}
}
} else {
// 清理指定实例
const assistant = ImageCaption.getInstance(nodeId);
if (assistant) {
this._cleanupSingleInstance(assistant);
ImageCaption.instances.delete(String(nodeId));
if (!silent) {
logger.log(`清理图像小助手实例 | ID: ${nodeId}`);
}
}
}
} catch (error) {
logger.error(`清理图像小助手实例失败 | ${error.message}`);
}
}
/**
* 清理单个实例的内部方法
* @param {object} assistant - 小助手实例
*/
_cleanupSingleInstance(assistant) {
if (!assistant) return;
try {
// 清理DOM元素
if (assistant.element && assistant.element.parentNode) {
assistant.element.parentNode.removeChild(assistant.element);
}
// 清理事件监听器
if (assistant._eventCleanupFunctions && Array.isArray(assistant._eventCleanupFunctions)) {
assistant._eventCleanupFunctions.forEach(cleanup => {
if (typeof cleanup === 'function') {
cleanup();
}
});
assistant._eventCleanupFunctions = [];
}
// 清理定时器
if (assistant._timers) {
Object.values(assistant._timers).forEach(timer => {
if (timer) {
clearTimeout(timer);
}
});
assistant._timers = {};
}
// 清理引用
assistant.element = null;
assistant.innerContent = null;
assistant.hoverArea = null;
assistant.indicator = null;
assistant.buttons = {};
assistant.node = null;
} catch (error) {
logger.error(`清理单个实例失败 | ${error.message}`);
}
}
/**
* 统一控制总开关功能
*/
async toggleGlobalFeature(enable, force = false) {
// 更新状态
const oldValue = window.FEATURES.imageCaption;
window.FEATURES.imageCaption = enable;
// 状态未变化时不执行操作,除非force为true
if (!force && oldValue === enable) {
return;
}
// 仅当状态变化或强制执行时才记录日志
if (oldValue !== enable || force) {
logger.log(`图像反推功能 | 动作:${enable ? "启用" : "禁用"}`);
}
try {
if (enable) {
// === 启用图像反推功能 ===
// 确保管理器已初始化
if (!EventManager.initialized) {
EventManager.init();
}
// 1. 重置节点初始化标记,准备重新检测
if (app.canvas && app.canvas.graph) {
const nodes = app.canvas.graph._nodes || [];
nodes.forEach(node => {
if (node) {
node._imageCaptionInitialized = false;
}
});
}
// 2. 设置或恢复节点选择事件监听
this.registerNodeSelectionListener();
// 3. 检查当前选中的节点
if (app.canvas && app.canvas.selected_nodes) {
app.canvas._imageCaptionSelectionHandler(app.canvas.selected_nodes);
}
} else {
// === 禁用图像反推功能 ===
// 1. 清理所有实例
const instanceCount = ImageCaption.instances.size;
this.cleanup(null, true);
}
// 更新按钮可见性
if (window.FEATURES.updateButtonsVisibility) {
window.FEATURES.updateButtonsVisibility();
}
} catch (error) {
logger.error(`图像小助手功能开关操作失败 | 错误: ${error.message}`);
}
}
}
// 创建单例实例
const imageCaption = new ImageCaption();
// 导出
export { imageCaption, ImageCaption };