572 lines
22 KiB
JavaScript
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 };
|