Files

572 lines
22 KiB
JavaScript

/**
* 提示词小助手 (PromptAssistant) 主入口文件
* 负责扩展初始化、节点检测和功能注入
*/
import { app } from "../../../scripts/app.js";
import { promptAssistant, PromptAssistant } from './modules/PromptAssistant.js';
import { registerSettings } from './modules/settings.js';
import { FEATURES as ASSISTANT_FEATURES, handleFeatureChange, setFeatureModuleDeps } from './services/features.js';
import { EventManager } from './utils/eventManager.js';
import { ResourceManager } from './utils/resourceManager.js';
import { UIToolkit } from "./utils/UIToolkit.js";
import { logger } from './utils/logger.js';
import { HistoryCacheService, TagCacheService } from './services/cache.js';
import { imageCaption, ImageCaption } from './modules/imageCaption.js';
import { nodeHelpTranslator } from './modules/nodeHelpTranslator.js';
import { nodeMountService, RENDER_MODE } from './services/NodeMountService.js';
import { tUI } from "./utils/uiI18n.js";
import './node/captionFrame.js'; // 导入视频手动抽帧功能
// ====================== 全局配置与状态 ======================
// 设置全局对象供其他模块访问
window.FEATURES = ASSISTANT_FEATURES;
// 将实例添加到全局对象
window.promptAssistant = promptAssistant;
window.imageCaption = imageCaption;
// 将实例添加到全局app对象
app.promptAssistant = promptAssistant;
app.imageCaption = imageCaption;
// ====================== 扩展注册 ======================
/**
* 注册ComfyUI扩展
*/
app.registerExtension({
name: "Comfy.PromptAssistant",
// ---扩展生命周期钩子---
/**
* 初始化扩展
*/
async setup() {
try {
// 初始化节点挂载服务(需要在其他初始化之前)
nodeMountService.initialize();
// 注册渲染模式切换处理
nodeMountService.onModeChange(async (newMode, oldMode) => {
logger.log(`[index] 渲染模式切换检测 | ${oldMode} -> ${newMode}`);
// 重新初始化所有小助手
if (window.FEATURES.enabled) {
// 先清理所有现有实例
promptAssistant.cleanup(null, true);
imageCaption.cleanup(null, true);
// 等待一帧确保 DOM 更新
await new Promise(resolve => requestAnimationFrame(resolve));
await promptAssistant.toggleGlobalFeature(true, true);
if (window.FEATURES.imageCaption) {
await imageCaption.toggleGlobalFeature(true, true);
}
logger.log(`[index] 渲染模式切换后重新初始化完成`);
}
});
// 注册设置选项
await registerSettings();
// 初始化自动翻译拦截器(独立于提示词小助手)
// 初始化提示词小助手(内部会处理版本号检查和总开关状态)
await promptAssistant.initialize();
// 初始化图像小助手(只初始化一次)
if (!imageCaption.initialized) {
await imageCaption.initialize();
}
// 清理旧引用
if (app.canvas) {
app.canvas.updateNodeAssistantsVisibility = null;
app.canvas._onNodeSelectionChange = null;
}
// 将管理器添加到app对象,使其可以通过window.app访问
app.EventManager = EventManager;
app.ResourceManager = ResourceManager;
app.UIToolkit = UIToolkit;
// 先初始化 features.js 依赖
setFeatureModuleDeps({
promptAssistant,
PromptAssistant,
UIToolkit,
HistoryCacheService,
TagCacheService,
imageCaption,
ImageCaption,
nodeHelpTranslator
});
// 然后再自动注册服务功能
if (window.FEATURES.enabled) {
await promptAssistant.toggleGlobalFeature(true, true);
// 避免重复初始化,只在必要时启用图像小助手功能
if (window.FEATURES.imageCaption) {
await imageCaption.toggleGlobalFeature(true, false);
}
// 初始化节点帮助翻译模块(根据功能开关)
if (window.FEATURES.nodeHelpTranslator) {
nodeHelpTranslator.initialize();
}
}
logger.debug("扩展初始化完成");
} catch (error) {
logger.error(`扩展初始化失败: ${error.message}`);
}
// 延迟hook Note/MarkdownNote/PreviewAny节点类型
setTimeout(() => {
try {
const NoteNodeType = LiteGraph.registered_node_types['Note'];
const MarkdownNoteNodeType = LiteGraph.registered_node_types['MarkdownNote'];
const PreviewAnyNodeType = LiteGraph.registered_node_types['PreviewAny'];
const PreviewTextNodeType = LiteGraph.registered_node_types['PreviewTextNode'];
if (NoteNodeType) this._hookNoteNodeType(NoteNodeType, 'Note');
if (MarkdownNoteNodeType) this._hookNoteNodeType(MarkdownNoteNodeType, 'MarkdownNote');
if (PreviewAnyNodeType) this._hookNoteNodeType(PreviewAnyNodeType, 'PreviewAny');
if (PreviewTextNodeType) this._hookNoteNodeType(PreviewTextNodeType, 'PreviewTextNode');
// 可能的其他名称变体
const altNames = ['PreviewText', 'Preview as Text', 'Markdown Preview'];
altNames.forEach(name => {
const nodeType = LiteGraph.registered_node_types[name];
if (nodeType) {
this._hookNoteNodeType(nodeType, name);
logger.debug(`[setup] 注入Preview节点成功 | 类型: ${name}`);
}
});
} catch (error) {
logger.error(`[setup] Hook Note节点失败: ${error.message}`);
}
}, 50);
// ---全局节点监听---
this._bindGraphHooks(app.graph);
// ---子图进入/退出监听(Vue Node 2.0 自动创建支持)---
this._setupGraphSwitchListener();
// 暴露 _injectUniversalHooks 供外部使用
app.registerExtension._injectUniversalHooks = this._injectUniversalHooks.bind(this);
// --- 监听后端警告事件 (如帧数截断) ---
app.api.addEventListener("prompt_assistant.warning", ({ detail }) => {
if (detail?.type === "frame_truncated") {
const summary = tUI("帧数已截断", "⚠️ Frames Truncated");
const detailMsg = tUI("帧数截断详情", "Model supports max {max} frames, {truncated} frame(s) ignored")
.replace("{max}", detail.max_images)
.replace("{truncated}", detail.truncated);
app.extensionManager?.toast?.add({
severity: "warn",
summary: summary,
detail: detailMsg,
life: 8000,
});
}
});
},
/**
* 设置画布 graph 切换监听器
* 检测进入/退出子图事件,自动创建模式下重新扫描节点
*/
_setupGraphSwitchListener() {
if (!app.canvas) return;
// 记录上一次的 graph 引用
let lastGraph = app.canvas.graph;
const self = this;
// 通过 Object.defineProperty hook app.canvas.graph 的 setter
// 当 graph 切换(进入/退出子图)时触发扫描
const originalDescriptor = Object.getOwnPropertyDescriptor(app.canvas, 'graph') || {
value: app.canvas.graph,
writable: true,
configurable: true
};
// 备份以便调试恢复
window.__PA_ORIG_GRAPH_DESC__ = originalDescriptor;
// 保存原始值
let _graphValue = app.canvas.graph;
Object.defineProperty(app.canvas, 'graph', {
get() {
return _graphValue;
},
set(newGraph) {
const oldGraph = _graphValue;
_graphValue = newGraph;
// 如果有原始 setter,调用它
if (originalDescriptor.set) {
originalDescriptor.set.call(this, newGraph);
}
// 检测 graph 切换
if (newGraph && newGraph !== oldGraph) {
logger.debug(`[graphSwitch] 检测到画布切换 | 旧Graph: ${oldGraph?._workflow_id || 'unknown'} -> 新Graph: ${newGraph?._workflow_id || 'unknown'}`);
// 延迟执行,确保画布切换完成
const isVueMode = typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode === true;
const delay = isVueMode ? 300 : 100;
setTimeout(() => {
self._onGraphSwitch(newGraph);
}, delay);
}
},
configurable: true,
enumerable: true
});
logger.debug('[graphSwitch] 画布切换监听器已设置');
},
/**
* 画布切换后的处理逻辑
* 复用 _bindGraphHooks 的扫描逻辑,避免代码重复
* @param {object} graph - 新的 graph 对象
*/
_onGraphSwitch(graph) {
if (!graph || !window.FEATURES.enabled) return;
// 调用已有的绑定钩子方法,并传入 resetFlags 选项以重置节点初始化标记
this._bindGraphHooks(graph, { resetFlags: true });
},
/**
* 为指定 graph 绑定节点挂载钩子
* 支持主画布和子图内部
* @param {object} graph - graph 对象
* @param {object} options - 选项 { resetFlags: 是否重置节点初始化标记 }
*/
_bindGraphHooks(graph, options = {}) {
if (!graph) return;
const { resetFlags = false } = options;
// 绑定钩子(只执行一次)
if (!graph._promptAssistantHooksInjected) {
graph._promptAssistantHooksInjected = true;
const origOnNodeAdded = graph.onNodeAdded;
graph.onNodeAdded = (node) => {
if (origOnNodeAdded) origOnNodeAdded.apply(graph, [node]);
if (!window.FEATURES.enabled || !node) return;
// 1. 动态注入 Hooks (onSelected, onRemoved)
this._injectUniversalHooks(node);
// 2. 自动挂载尝试
this._handleNodeActive(node, { delay: true });
};
// logger.log(`[graphHooks] 已绑定 graph 钩子 | ID: ${graph._workflow_id || graph.constructor?.name || 'unknown'}`);
// 【关键】处理进入子图时已存在的节点
// Vue 模式需要更长延迟,确保 DOM 渲染完成
const isVueMode = typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode === true;
const scanDelay = isVueMode ? 500 : 100;
const scanExistingNodes = () => {
if (!window.FEATURES.enabled) return;
const creationMode = app.ui.settings.getSettingValue("PromptAssistant.Settings.CreationMode") || "auto";
const icCreationMode = app.ui.settings.getSettingValue("PromptAssistant.Settings.ImageCaptionCreationMode") || "auto";
// 只要任意一个模块开启了自动创建,就需要扫描现有节点
if (creationMode !== "auto" && icCreationMode !== "auto") {
// logger.debugSample(() => `[graphHooks] 跳过初始扫描 | PA模式: ${creationMode} | IC模式: ${icCreationMode}`);
return;
}
const nodes = graph._nodes || [];
if (nodes.length === 0) return;
nodes.forEach(node => {
if (!node || node.id === -1) return;
// 1. 注入钩子 (确保 onSelected/onRemoved 等能正常工作)
this._injectUniversalHooks(node);
// 2. 统一分发到激活处理函数,它内部会根据各自的自动创建设置进行判断
this._handleNodeActive(node, { delay: false });
});
};
setTimeout(scanExistingNodes, scanDelay);
}
// 【新增】如果需要重置标记(子图切换场景),立即扫描现有节点
if (resetFlags) {
// 切换图时,先清理当前 UI 实例(不清理缓存),防止 ID 冲突的残留
promptAssistant.cleanup(null, true);
const isVueMode = typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode === true;
const delay = isVueMode ? 300 : 100;
setTimeout(() => {
const nodes = graph._nodes || [];
nodes.forEach(node => {
if (!node || node.id === -1) return;
// 重置初始化标记,允许重新创建
node._promptAssistantInitialized = false;
node._imageCaptionInitialized = false;
// 注入钩子
this._injectUniversalHooks(node);
// 触发自动创建
this._handleNodeActive(node, { delay: false });
});
if (nodes.length > 0) {
const gId = graph.id || graph._workflow_id || 'main';
logger.debug(`[graphSwitch] 自动扫描完成 | 节点数: ${nodes.length} | Graph: ${gId}`);
}
}, delay);
}
},
/**
* 为所有节点注入通用的交互钩子 (onSelected, onRemoved)
* 特别是针对动态创建的子图节点,确确保能够响应点击和资源清理
* @param {object} node - LiteGraph 节点实例
*/
_injectUniversalHooks(node) {
if (!node || node._promptAssistantHooksInjected) return;
const self = this;
const origOnSelected = node.onSelected;
const origOnRemoved = node.onRemoved;
// 实例级覆盖 (针对动态创建或特殊节点)
node.onSelected = function () {
if (origOnSelected) origOnSelected.apply(this, arguments);
self._handleNodeActive(this, { reset: true, delay: true });
};
node.onRemoved = function () {
self._handleNodeCleanup(this);
if (origOnRemoved) origOnRemoved.apply(this, arguments);
};
node._promptAssistantHooksInjected = true;
},
/**
* @deprecated 已由 _injectUniversalHooks 替代,保留用于注册时的遗留支持
*/
_hookNoteNodeType(NodeType, typeName) {
if (!NodeType || !NodeType.prototype) return;
// 我们不再重写原型方法,而是通过 onNodeAdded 动态注入实例方法
// 这在 Node 2.0 动态创建时更可靠
// logger.debug(`[_hookNoteNodeType] 类型已注册: ${typeName}`);
},
// ---其他方法保持不变---
async _setupOtherMethods() {
const self = this;
// 仅保留工作流ID识别功能,不处理工作流切换事件
try {
const LGraph = app.graph.constructor;
const origConfigure = LGraph.prototype.configure;
LGraph.prototype.configure = function (data) {
// 在图表对象上存储工作流ID
this._workflow_id = data.id || LiteGraph.uuidv4();
// 执行原始方法
return origConfigure.apply(this, arguments);
};
// 添加工作流加载监听,只标记切换状态,不做特殊处理
const origLoadGraphData = app.loadGraphData;
app.loadGraphData = async function (data) {
// 设置工作流切换标记,避免删除缓存
window.PROMPT_ASSISTANT_WORKFLOW_SWITCHING = true;
// 简化日志:仅在工作流ID变化时打印一次
const workflowId = data?.id || (data?.extra?.workflow_id) || "未知工作流";
if (app.graph?._workflow_id !== workflowId) {
logger.log(`[工作流] 切换: ${workflowId}`);
}
try {
// 调用原始加载方法
const result = await origLoadGraphData.apply(this, arguments);
// 工作流加载完成后,统一处理现有节点的激活(包括自动创建判定)
requestAnimationFrame(() => {
if (app.graph && app.graph._nodes) {
app.graph._nodes.forEach(node => {
if (node && node.id !== -1) {
self._handleNodeActive(node, { delay: false });
}
});
}
});
return result;
} finally {
// 延迟重置工作流切换标记
setTimeout(() => {
window.PROMPT_ASSISTANT_WORKFLOW_SWITCHING = false;
}, 500);
}
};
} catch (e) {
logger.error("[PromptAssistant] 注入 LGraph 设置工作流ID失败", e);
}
},
// ---节点生命周期钩子---
/**
* 节点创建钩子
* 在节点创建时初始化特定类型节点的小助手
*/
async nodeCreated(node) {
// nodeCreated 钩子现在主要用于补齐子图节点的特殊交互,大部分逻辑已通过 onNodeCreated 注入
if (!node || node.id === -1) return;
this._injectUniversalHooks(node);
},
async nodeRemoved(node) {
if (window.PROMPT_ASSISTANT_WORKFLOW_SWITCHING) return;
this._handleNodeCleanup(node);
},
/**
* 节点定义注册前钩子
* 向所有节点类型注入小助手相关功能
*/
// --- 统一生命周期管理逻辑 (重构点) ---
/**
* 统一处理节点的“进入/激活”逻辑
* 涵盖:新节点创建(onNodeCreated), 全局节点添加(onNodeAdded), 节点选中(onSelected)
* @param {object} node - 节点实例
* @param {object} options - 配置参数 { reset: 是否强制重置标记, delay: 是否使用 raf 延迟 }
*/
_handleNodeActive(node, options = {}) {
if (!node || !window.FEATURES.enabled) return;
if (node.id === -1) return;
const { reset = false, delay = true } = options;
if (reset) {
node._promptAssistantInitialized = false;
node._imageCaptionInitialized = false;
}
const run = () => {
if (!node || !node.id || node.id === -1) return;
// 1. 提示词小助手核心入口
if (PromptAssistant.isValidNode(node)) {
const creationMode = app.ui.settings.getSettingValue("PromptAssistant.Settings.CreationMode") || "auto";
if ((creationMode === "auto" || reset) && !node._promptAssistantInitialized) {
node._promptAssistantInitialized = true;
promptAssistant.checkAndSetupNode(node);
}
}
// 2. 图像反推小助手入口
const isSupportedICNode = imageCaption.isSupportedNode && imageCaption.isSupportedNode(node);
if (window.FEATURES.imageCaption && isSupportedICNode) {
const icCreationMode = app.ui.settings.getSettingValue("PromptAssistant.Settings.ImageCaptionCreationMode") || "auto";
if (reset && app.canvas?._imageCaptionSelectionHandler) {
node._imageCaptionInitialized = false;
app.canvas._imageCaptionSelectionHandler({ [node.id]: node });
} else if (icCreationMode === "auto" && !node._imageCaptionInitialized) {
node._imageCaptionInitialized = true;
imageCaption.checkAndSetupNode(node);
}
}
};
if (delay) {
requestAnimationFrame(() => requestAnimationFrame(run));
} else {
run();
}
},
/**
* 统一处理节点的“销毁/清理”逻辑
* @param {object} node - 节点实例
*/
_handleNodeCleanup(node) {
if (!node || node.id === undefined || node.id === -1) return;
const nodeId = node.id;
// 执行清理并标记状态
if (node._promptAssistantInitialized || !node._promptAssistantCleaned) {
promptAssistant.cleanup(nodeId, false);
node._promptAssistantCleaned = true;
}
if (node._imageCaptionInitialized || !node._imageCaptionCleaned) {
imageCaption.cleanup(nodeId, false);
node._imageCaptionCleaned = true;
}
},
/**
* 注册前的批量原型注入
*/
async beforeRegisterNodeDef(nodeType, nodeData) {
const self = this;
const proto = nodeType.prototype;
const origOnCreated = proto.onNodeCreated;
const origOnSelected = proto.onSelected;
const origOnRemoved = proto.onRemoved;
// 注入创建钩子 (原型级补救)
proto.onNodeCreated = function () {
if (origOnCreated) origOnCreated.apply(this, arguments);
self._handleNodeActive(this, { delay: true });
};
// 注入选中钩子 (原型级补救)
proto.onSelected = function () {
if (origOnSelected) origOnSelected.apply(this, arguments);
self._handleNodeActive(this, { reset: true, delay: true });
};
// 注入移除钩子 (原型级补救)
proto.onRemoved = function () {
self._handleNodeCleanup(this);
if (origOnRemoved) origOnRemoved.apply(this, arguments);
};
},
/**
* 扩展卸载钩子
* 在扩展被卸载时清理所有资源
*/
async beforeExtensionUnload() {
promptAssistant.cleanup();
imageCaption.cleanup();
}
});
export { EventManager, UIToolkit };