2.0.3
This commit is contained in:
+7
-3
@@ -293,6 +293,9 @@ app.registerExtension({
|
||||
|
||||
// 【新增】如果需要重置标记(子图切换场景),立即扫描现有节点
|
||||
if (resetFlags) {
|
||||
// 切换图时,先清理当前 UI 实例(不清理缓存),防止 ID 冲突的残留
|
||||
promptAssistant.cleanup(null, true);
|
||||
|
||||
const isVueMode = typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode === true;
|
||||
const delay = isVueMode ? 300 : 100;
|
||||
|
||||
@@ -313,7 +316,8 @@ app.registerExtension({
|
||||
});
|
||||
|
||||
if (nodes.length > 0) {
|
||||
logger.debug(`[graphSwitch] 自动扫描完成 | 节点数: ${nodes.length}`);
|
||||
const gId = graph.id || graph._workflow_id || 'main';
|
||||
logger.debug(`[graphSwitch] 自动扫描完成 | 节点数: ${nodes.length} | Graph: ${gId}`);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
@@ -358,7 +362,7 @@ app.registerExtension({
|
||||
|
||||
// ---其他方法保持不变---
|
||||
async _setupOtherMethods() {
|
||||
|
||||
const self = this;
|
||||
// 仅保留工作流ID识别功能,不处理工作流切换事件
|
||||
try {
|
||||
const LGraph = app.graph.constructor;
|
||||
@@ -392,7 +396,7 @@ app.registerExtension({
|
||||
if (app.graph && app.graph._nodes) {
|
||||
app.graph._nodes.forEach(node => {
|
||||
if (node && node.id !== -1) {
|
||||
this._handleNodeActive(node, { delay: false });
|
||||
self._handleNodeActive(node, { delay: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -299,6 +299,18 @@ class PromptAssistant {
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心优化】统一获取助手实例的唯一键名
|
||||
* 解决子图节点 ID 冲突及不同扫描模式下的键名不一致问题
|
||||
*/
|
||||
_getAssistantKey(node, inputId) {
|
||||
if (!node) return null;
|
||||
const graph = node.graph || app.graph;
|
||||
// 优先顺序:graph.id (Locator ID) -> graph._workflow_id -> 'main'
|
||||
const graphId = graph?.id || graph?._workflow_id || 'main';
|
||||
return `${graphId}_${node.id}_${inputId}`;
|
||||
}
|
||||
|
||||
// ---生命周期管理功能---
|
||||
/**
|
||||
* 判断功能是否被禁用
|
||||
@@ -494,14 +506,23 @@ class PromptAssistant {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查nodeId是否有效
|
||||
// 检查Id是否有效
|
||||
if (nodeId !== null && nodeId !== undefined) {
|
||||
// 确保nodeId是字符串类型,便于后续比较
|
||||
const nodeIdStr = String(nodeId);
|
||||
const searchId = String(nodeId);
|
||||
|
||||
// 获取当前节点的所有实例键
|
||||
// 获取所有匹配的实例键
|
||||
// 逻辑:匹配精确键 (graphId_nodeId_inputId) 或者以 nodeId_ 开头的旧键,或者包含 _nodeId_ 的全量键
|
||||
const keysToDelete = Array.from(PromptAssistant.instances.keys())
|
||||
.filter(key => key === nodeIdStr || key.startsWith(`${nodeIdStr}_`));
|
||||
.filter(key => {
|
||||
// 1. 精确匹配(如果传入的是 assistantKey)
|
||||
if (key === searchId) return true;
|
||||
// 2. 匹配 nodeId (旧格式)
|
||||
if (key.startsWith(`${searchId}_`)) return true;
|
||||
// 3. 匹配带 graphId 前缀的格式 (graphId_nodeId_inputId)
|
||||
const parts = key.split('_');
|
||||
return parts.length >= 2 && parts[1] === searchId;
|
||||
});
|
||||
|
||||
// 如果有实例需要清理
|
||||
if (keysToDelete.length > 0) {
|
||||
@@ -679,17 +700,17 @@ class PromptAssistant {
|
||||
/**
|
||||
* 获取实例
|
||||
*/
|
||||
static getInstance(nodeId) {
|
||||
if (nodeId == null) return null;
|
||||
return this.instances.get(String(nodeId));
|
||||
static getInstance(key) {
|
||||
if (key == null) return null;
|
||||
return this.instances.get(String(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实例是否存在
|
||||
*/
|
||||
static hasInstance(nodeId) {
|
||||
if (nodeId == null) return false;
|
||||
return this.instances.has(String(nodeId));
|
||||
static hasInstance(key) {
|
||||
if (key == null) return false;
|
||||
return this.instances.has(String(key));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -745,9 +766,8 @@ class PromptAssistant {
|
||||
validInputs.forEach((inputWidget, widgetIndex) => {
|
||||
const inputId = inputWidget.name || inputWidget.id;
|
||||
|
||||
// 生成唯一的 assistantKey
|
||||
// 对于同名的多个输入框(如 Show Any 节点的列表输入),使用索引区分
|
||||
let assistantKey = `${node.id}_${inputId}`;
|
||||
// --- 核心修复:多图支持的唯一键 ---
|
||||
let assistantKey = this._getAssistantKey(node, inputId);
|
||||
|
||||
// 检查是否存在同名的输入框,如果存在则使用索引或 DOM 元素的唯一标识
|
||||
const sameNameWidgets = validInputs.filter(w => (w.name || w.id) === inputId);
|
||||
@@ -757,43 +777,49 @@ class PromptAssistant {
|
||||
if (inputEl) {
|
||||
// 为输入框元素添加唯一标识
|
||||
if (!inputEl.dataset.promptAssistantUniqueId) {
|
||||
inputEl.dataset.promptAssistantUniqueId = `${node.id}_${inputId}_${widgetIndex}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
inputEl.dataset.promptAssistantUniqueId = `${graphId}_${node.id}_${inputId}_${widgetIndex}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
assistantKey = inputEl.dataset.promptAssistantUniqueId;
|
||||
} else {
|
||||
// 降级方案:使用索引
|
||||
assistantKey = `${node.id}_${inputId}_${widgetIndex}`;
|
||||
assistantKey = `${graphId}_${node.id}_${inputId}_${widgetIndex}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查实例是否已存在
|
||||
if (PromptAssistant.hasInstance(assistantKey)) {
|
||||
// 如果实例存在,检查输入控件是否已更新
|
||||
// 如果实例存在,检查输入控件是否已更新,或者 UI 元素是否已丢失
|
||||
const instance = PromptAssistant.getInstance(assistantKey);
|
||||
const currentInputEl = inputWidget.inputEl;
|
||||
const instanceInputEl = instance?.text_element;
|
||||
const instanceUIEl = instance?.element;
|
||||
|
||||
// 只有在以下情况下才清理并重建:
|
||||
// 1. 两个元素都存在
|
||||
// 2. 它们是不同的元素
|
||||
// 3. 不是因为临时的 DOM 状态变化(如打开弹窗)
|
||||
if (instanceInputEl && currentInputEl && instanceInputEl !== currentInputEl) {
|
||||
// 检查 UI 元素是否仍然在 DOM 中
|
||||
const isVueMode = nodeMountService.isVueNodesMode();
|
||||
const nodeContainer = isVueMode ? document.querySelector(`[data-node-id="${node.id}"]`) : null;
|
||||
const isUIPresent = isVueMode ? nodeContainer?.contains(instanceUIEl) : document.body.contains(instanceUIEl);
|
||||
|
||||
// --- 修复:处理挂载竞争状态 ---
|
||||
// 如果 UI 元素丢失,或者输入元素引用变化且原元素已移除,则清理并重建
|
||||
// 增加 _isMounting 标记检查,避免异步挂载期间被误判为丢失
|
||||
if (!instanceUIEl || (!isUIPresent && !instance._isMounting)) {
|
||||
logger.debug(() => `[checkAndSetupNode] UI 元素已丢失,清理实例以触发重建 | 节点ID: ${node.id} | 键: ${assistantKey}`);
|
||||
// 传入完整的 assistantKey 以确保精确清理
|
||||
this.cleanup(assistantKey);
|
||||
} else if (!isUIPresent && instance._isMounting) {
|
||||
// 正在挂载中,跳过
|
||||
return;
|
||||
} else if (instanceInputEl && currentInputEl && instanceInputEl !== currentInputEl) {
|
||||
// 进一步检查:确保确实需要重建(避免误判)
|
||||
// 如果当前元素已经从 DOM 中移除,才需要清理
|
||||
if (!document.body.contains(instanceInputEl)) {
|
||||
logger.debug(() => `[checkAndSetupNode] 输入元素已失效,清理实例 | 节点ID: ${node.id}`);
|
||||
this.cleanup(node.id);
|
||||
} else {
|
||||
// 元素仍然在 DOM 中,可能只是引用变化,不需要清理
|
||||
// logger.debug(() => `[checkAndSetupNode] 输入元素引用变化但仍有效,跳过清理 | 节点ID: ${node.id}`);
|
||||
return;
|
||||
}
|
||||
} else if (!currentInputEl && instanceInputEl) {
|
||||
// Vue 模式下,inputEl 可能暂时为 null,不应该触发清理
|
||||
logger.debug(() => `[checkAndSetupNode] 当前inputEl为null,跳过清理(Vue模式下可能暂时为null) | 节点ID: ${node.id}`);
|
||||
return;
|
||||
} else {
|
||||
// 实例存在且未更新,跳过
|
||||
// 实例存在且一切正常,跳过
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -867,11 +893,28 @@ class PromptAssistant {
|
||||
* 执行实际的 DOM 挂载
|
||||
*/
|
||||
_mountDomAssistant(node, element, inputId, index) {
|
||||
const assistantKey = `${node.id}_${inputId}`;
|
||||
if (PromptAssistant.hasInstance(assistantKey)) return;
|
||||
const assistantKey = this._getAssistantKey(node, inputId);
|
||||
if (PromptAssistant.hasInstance(assistantKey)) {
|
||||
const instance = PromptAssistant.getInstance(assistantKey);
|
||||
const isVueMode = nodeMountService.isVueNodesMode();
|
||||
const nodeContainer = isVueMode ? document.querySelector(`[data-node-id="${node.id}"]`) : null;
|
||||
const isUIPresent = isVueMode ? nodeContainer?.contains(instance?.element) : document.body.contains(instance?.element);
|
||||
|
||||
// 检查元素是否已被挂载
|
||||
if (element._promptAssistantMounted) return;
|
||||
if (instance?.element && isUIPresent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 实例存在但 UI 丢失,清理旧实例以便重建
|
||||
logger.debug(() => `[_mountDomAssistant] 检测到孤立实例,清理重建 | 节点ID: ${node.id}`);
|
||||
this.cleanup(node.id);
|
||||
}
|
||||
|
||||
// 检查元素是否已被挂载 (基于 DOM 属性判断)
|
||||
if (element._promptAssistantMounted) {
|
||||
// 如果属性还在但实例在 Map 中没了,或者 UI 确实不可见了,应该允许重新挂载
|
||||
// 这里我们保持原样,通过上面的 cleanup 保证一致性
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建虚拟 widget
|
||||
const virtualWidget = {
|
||||
@@ -1021,7 +1064,7 @@ class PromptAssistant {
|
||||
}
|
||||
|
||||
const nodeId = node.id;
|
||||
const widgetKey = assistantKey || `${nodeId}_${inputId}`;
|
||||
const widgetKey = assistantKey || this._getAssistantKey(node, inputId);
|
||||
|
||||
|
||||
|
||||
@@ -1044,6 +1087,7 @@ class PromptAssistant {
|
||||
text_element: inputEl,
|
||||
inputEl: inputEl,
|
||||
isDestroyed: false,
|
||||
_isMounting: true, // 标记挂载中状态
|
||||
nodeInfo: {
|
||||
...nodeInfo,
|
||||
nodeId: nodeId,
|
||||
@@ -1295,6 +1339,11 @@ class PromptAssistant {
|
||||
|
||||
this._setupUIPosition(widget, inputEl, containerEl, canvasContainerRect, (success) => {
|
||||
|
||||
if (widget.isDestroyed) {
|
||||
logger.debug(`[定位] 回调跳过:实例已销毁 | ID: ${nodeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
logger.debugSample(() => `[小助手] 创建暂缓 | 节点ID: ${nodeId} | 原因: 定位容器未就绪 (等待DOM渲染)`);
|
||||
container.destroy();
|
||||
@@ -1310,6 +1359,8 @@ class PromptAssistant {
|
||||
|
||||
// 定位成功后更新尺寸
|
||||
container.updateDimensions();
|
||||
// 挂载完成,清除标记
|
||||
widget._isMounting = false;
|
||||
});
|
||||
|
||||
return containerEl;
|
||||
|
||||
+82
-22
@@ -144,6 +144,19 @@ class ImageCaption {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 补充检测: Vue Node 2.0 DOM 检测 ---
|
||||
if (!result.hasValidImage && nodeMountService.isVueNodesMode()) {
|
||||
const nodeContainer = document.querySelector(`[data-node-id="${node.id}"]`);
|
||||
if (nodeContainer) {
|
||||
const imgElement = nodeContainer.querySelector('img');
|
||||
if (imgElement && imgElement.src && !imgElement.src.includes('error')) {
|
||||
// logger.debugSample(() => `[图像小助手] 通过 DOM 检测到图像 | ID: ${node.id}`);
|
||||
result.hasValidImage = true;
|
||||
result.isValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -298,9 +311,7 @@ class ImageCaption {
|
||||
// 检查节点是否已被删除(只有在删除时才清理实例)
|
||||
if (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[node.id]) {
|
||||
// 节点已被删除,清理实例
|
||||
if (ImageCaption.hasInstance(node.id)) {
|
||||
this.cleanup(node.id);
|
||||
}
|
||||
this.cleanup(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,7 +331,14 @@ class ImageCaption {
|
||||
|
||||
// 验证现有实例是否有效
|
||||
const existingInstance = ImageCaption.getInstance(node.id);
|
||||
if (existingInstance && existingInstance.element && document.body.contains(existingInstance.element)) {
|
||||
const isElementValid = existingInstance && existingInstance.element && document.body.contains(existingInstance.element);
|
||||
const isVueMode = nodeMountService.isVueNodesMode();
|
||||
|
||||
// 在 Vue 模式下,element 可能挂载在节点容器内而不是 body
|
||||
const isVueElementValid = isVueMode && existingInstance && existingInstance.element &&
|
||||
document.querySelector(`[data-node-id="${node.id}"]`)?.contains(existingInstance.element);
|
||||
|
||||
if (existingInstance && (isElementValid || isVueElementValid)) {
|
||||
// 实例有效,更新其显示状态(内部会处理图片检测)
|
||||
this.updateAssistantVisibility(existingInstance);
|
||||
} else {
|
||||
@@ -1678,6 +1696,23 @@ class ImageCaption {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查节点是否已被删除
|
||||
if (!node) {
|
||||
this.cleanup(assistant.nodeId);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 修复:LiteGraph 子图“幽灵”图标问题 ---
|
||||
// 检查节点所属的 graph 是否为当前激活的 graph
|
||||
// 如果当前处在子图中,而节点在父图,则应隐藏
|
||||
// 注意:app.canvas.graph 是当前视图显示的 graph
|
||||
if (app.canvas && app.canvas.graph && node.graph && node.graph !== app.canvas.graph) {
|
||||
// 节点不在当前显示的 graph 中,强制隐藏
|
||||
// logger.debugSample(() => `[图像小助手] 节点不在当前视图中,强制隐藏 | ID: ${assistant.nodeId}`);
|
||||
this.hideAssistantUI(assistant);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用统一的状态检测方法
|
||||
const nodeState = this._checkNodeAndCanvasState(assistant.node);
|
||||
|
||||
@@ -1712,17 +1747,30 @@ class ImageCaption {
|
||||
// 如果节点是支持的类型但暂时没图,启动一个内部轻量级探测器
|
||||
if (!assistant._imageDetectionTimer) {
|
||||
const detectImage = () => {
|
||||
if (!assistant.node) return;
|
||||
// 检查实例是否已被清理或节点是否已不再图中
|
||||
if (assistant.isDestroyed || !assistant.node) {
|
||||
assistant._imageDetectionTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = this._checkNodeAndCanvasState(assistant.node);
|
||||
if (currentState.hasValidImage) {
|
||||
logger.debug(() => `[图像小助手] 异步图像加载成功 | ID: ${assistant.nodeId}`);
|
||||
assistant._imageDetectionTimer = null;
|
||||
this.updateAssistantVisibility(assistant);
|
||||
} else if (document.body.contains(assistant.element)) {
|
||||
|
||||
// 额外检查:如果 element 已经丢失(可能是 Vue 重新渲染),重新初始化
|
||||
const isVueMode = nodeMountService.isVueNodesMode();
|
||||
const nodeContainer = isVueMode ? document.querySelector(`[data-node-id="${assistant.nodeId}"]`) : null;
|
||||
const isElementPresent = isVueMode ? nodeContainer?.contains(assistant.element) : document.body.contains(assistant.element);
|
||||
|
||||
if (!assistant.element || !isElementPresent) {
|
||||
this.checkAndSetupNode(assistant.node);
|
||||
} else {
|
||||
this.updateAssistantVisibility(assistant);
|
||||
}
|
||||
} else {
|
||||
// 继续等待,每秒检测一次 (开销极低)
|
||||
assistant._imageDetectionTimer = setTimeout(detectImage, 1000);
|
||||
} else {
|
||||
assistant._imageDetectionTimer = null;
|
||||
}
|
||||
};
|
||||
assistant._imageDetectionTimer = setTimeout(detectImage, 1000);
|
||||
@@ -2301,11 +2349,17 @@ class ImageCaption {
|
||||
}
|
||||
} else {
|
||||
// 清理特定节点实例
|
||||
const assistant = ImageCaption.getInstance(nodeId);
|
||||
if (assistant) {
|
||||
this._cleanupSingleInstance(assistant);
|
||||
ImageCaption.instances.delete(String(nodeId));
|
||||
}
|
||||
const searchId = String(nodeId);
|
||||
const keysToDelete = Array.from(ImageCaption.instances.keys())
|
||||
.filter(key => key === searchId || key.startsWith(`${searchId}_`));
|
||||
|
||||
keysToDelete.forEach(key => {
|
||||
const assistant = ImageCaption.instances.get(key);
|
||||
if (assistant) {
|
||||
this._cleanupSingleInstance(assistant);
|
||||
ImageCaption.instances.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -2325,15 +2379,21 @@ class ImageCaption {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 清理指定实例
|
||||
const assistant = ImageCaption.getInstance(nodeId);
|
||||
if (assistant) {
|
||||
this._cleanupSingleInstance(assistant);
|
||||
ImageCaption.instances.delete(String(nodeId));
|
||||
if (!silent) {
|
||||
logger.log(`清理图像小助手实例 | ID: ${nodeId}`);
|
||||
// 清理指定节点的所有实例
|
||||
const searchId = String(nodeId);
|
||||
const keysToDelete = Array.from(ImageCaption.instances.keys())
|
||||
.filter(key => key === searchId || key.startsWith(`${searchId}_`));
|
||||
|
||||
keysToDelete.forEach(key => {
|
||||
const assistant = ImageCaption.instances.get(key);
|
||||
if (assistant) {
|
||||
this._cleanupSingleInstance(assistant);
|
||||
ImageCaption.instances.delete(key);
|
||||
if (!silent) {
|
||||
logger.log(`清理图像小助手实例 | 键: ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`清理图像小助手实例失败 | ${error.message}`);
|
||||
|
||||
+20
-11
@@ -15,7 +15,7 @@ class UIToolkit {
|
||||
static #statusTips = new Map(); // Map<buttonElement, tipElement>
|
||||
|
||||
// 支持的输入字段ID
|
||||
static VALID_INPUT_IDS = ["text", "text_positive", "text_negative", "text_g", "text_l"];
|
||||
static VALID_INPUT_IDS = ["text", "text_positive", "text_negative", "text_g", "text_l", "prompt", "string"];
|
||||
|
||||
// 状态文本管理
|
||||
static STATUS_TEXTS = {
|
||||
@@ -41,16 +41,18 @@ class UIToolkit {
|
||||
let isValid = false;
|
||||
let reason = '';
|
||||
|
||||
const widgetName = (widget.name || widget.id || '').toLowerCase();
|
||||
|
||||
// 方法1: 标准文本输入控件(传统litegraph模式)
|
||||
if (widget.inputEl && widget.inputEl.tagName === "TEXTAREA" &&
|
||||
this.VALID_INPUT_IDS.includes(widget.name)) {
|
||||
(this.VALID_INPUT_IDS.includes(widget.name) || widgetName.includes('prompt'))) {
|
||||
isValid = true;
|
||||
reason = 'litegraph textarea matched';
|
||||
}
|
||||
// 方法2: Note节点特殊输入
|
||||
// 方法2: Note节点或带文本框的特殊输入
|
||||
else if (widget.element && widget.element.tagName === "TEXTAREA") {
|
||||
isValid = true;
|
||||
reason = 'Note textarea matched';
|
||||
reason = 'Note/Element textarea matched';
|
||||
}
|
||||
// 方法3: Markdown Note节点特殊输入(Tiptap编辑器)
|
||||
else if (this.isMarkdownNoteInput(widget)) {
|
||||
@@ -65,18 +67,22 @@ class UIToolkit {
|
||||
|
||||
// ---可见性检测补救---
|
||||
// 如果基本检测通过,但元素本身是隐藏的,则视为无效
|
||||
// 注意:有些节点的 textarea 可能在初始化时 display:none,随后被 ComfyUI 脚本显示
|
||||
if (isValid) {
|
||||
const element = widget.inputEl || widget.element;
|
||||
if (element && !this.isElementVisible(element)) {
|
||||
isValid = false;
|
||||
reason += ' (but element is hidden)';
|
||||
// 如果是标准 textarea,尝试检查父级可见性,或者放宽判断(有些节点在后台渲染)
|
||||
const isMarkdown = this.isMarkdownNoteInput(widget);
|
||||
if (!isMarkdown) {
|
||||
isValid = false;
|
||||
reason += ' (but element is hidden)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
const widgetName = widget.name || widget.id || 'unknown';
|
||||
if (debug || (node && node.id === 74)) {
|
||||
const nodeType = node?.type || widget.node?.type || 'unknown';
|
||||
logger.debug(`[isValidInput] 控件: ${widgetName} | 节点类型: ${nodeType} | 有效: ${isValid} | 原因: ${reason}`);
|
||||
logger.debug(`[isValidInput] 节点[${node?.id || '?'}] | 控件: ${widget.name || '?'}| 类型: ${nodeType} | 有效: ${isValid} | 原因: ${reason}`);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
@@ -103,16 +109,19 @@ class UIToolkit {
|
||||
'PreviewAny', // Preview as Text节点(实际类型)
|
||||
'PreviewTextNode', // Preview节点可能的其他名称
|
||||
'Show any [Crystools]',
|
||||
'CLIPTextEncode', // 显式包含 CLIP 相关
|
||||
'CLIPTextEncodeSDXL',
|
||||
// 添加其他支持的节点类型
|
||||
];
|
||||
|
||||
// 检查节点类型
|
||||
if (supportedNodeTypes.includes(nodeRef.type)) {
|
||||
if (supportedNodeTypes.includes(nodeRef.type) || nodeRef.type?.includes('CLIP')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查控件名称是否为有效输入
|
||||
if (this.VALID_INPUT_IDS.includes(widget.name)) {
|
||||
const widgetName = (widget.name || widget.id || '').toLowerCase();
|
||||
if (this.VALID_INPUT_IDS.includes(widget.name) || widgetName.includes('prompt')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,9 @@ class HTTPClientPool:
|
||||
'verify': verify_ssl,
|
||||
'follow_redirects': True,
|
||||
'http2': False,
|
||||
# 关键修复:禁用系统环境变量代理配置
|
||||
# 避免 HTTP_PROXY/HTTPS_PROXY 导致 localhost 请求被代理拦截返回 502
|
||||
'trust_env': False,
|
||||
# 设置连接池保持连接
|
||||
'limits': httpx.Limits(max_keepalive_connections=10, max_connections=20, keepalive_expiry=60.0)
|
||||
}
|
||||
|
||||
+4
-1
@@ -190,7 +190,10 @@ class LLMService(OpenAICompatibleService):
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(final_timeout, connect=10.0, read=final_timeout),
|
||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
|
||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
||||
# 关键修复:禁用系统环境变量代理配置
|
||||
# 避免 HTTP_PROXY/HTTPS_PROXY 导致 localhost 请求被代理拦截返回 502
|
||||
trust_env=False
|
||||
) as client:
|
||||
full_content = ""
|
||||
|
||||
|
||||
@@ -165,7 +165,10 @@ def _fetch_ollama_models(base_url: str) -> Dict:
|
||||
'User-Agent': f'comfyui-prompt-assistant/1.0 ({platform.machine()} {platform.system().lower()}) Python/{platform.python_version()}'
|
||||
}
|
||||
|
||||
response = httpx.get(url, headers=headers, timeout=10.0)
|
||||
# 创建禁用代理的客户端,避免系统代理干扰localhost请求
|
||||
# 这是502错误的常见原因:系统代理无法正确转发localhost请求
|
||||
with httpx.Client(proxy=None, trust_env=False) as client:
|
||||
response = client.get(url, headers=headers, timeout=10.0)
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
@@ -178,6 +181,11 @@ def _fetch_ollama_models(base_url: str) -> Dict:
|
||||
"success": False,
|
||||
"error": f"Ollama返回400错误。详情: {error_detail}"
|
||||
}
|
||||
elif response.status_code == 502:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Ollama返回错误 (HTTP 502): 网关错误,请检查Ollama服务是否正常运行"
|
||||
}
|
||||
elif response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
|
||||
@@ -449,8 +449,8 @@ class OpenAICompatibleService(BaseAPIService):
|
||||
"keep_alive": 0
|
||||
}
|
||||
|
||||
# 创建临时客户端(卸载操作不需要复用)
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
# 创建临时客户端(卸载操作不需要复用,禁用代理避免localhost请求被拦截)
|
||||
async with httpx.AsyncClient(timeout=5.0, trust_env=False) as client:
|
||||
response = await client.post(url, json=payload)
|
||||
if response.status_code == 200:
|
||||
from ..utils.common import PROCESS_PREFIX
|
||||
|
||||
Reference in New Issue
Block a user