/**
* 小助手设置服务
* 负责管理小助手的设置选项,提供开关控制功能
*/
import { app } from "../../../../scripts/app.js";
import { logger } from '../utils/logger.js';
import { PromptAssistant, updateAutoTranslateIndicators } from "./PromptAssistant.js";
import { EventManager } from "../utils/eventManager.js";
import { ResourceManager } from "../utils/resourceManager.js";
import { HistoryCacheService, TagCacheService, TranslateCacheService, CACHE_CONFIG } from "../services/cache.js";
import { UIToolkit } from "../utils/UIToolkit.js";
import { FEATURES, handleFeatureChange } from "../services/features.js";
import { tagConfigManager } from "./tagConfigManager.js";
import { apiConfigManager } from "./apiConfigManager.js";
import { rulesConfigManager } from "./rulesConfigManager.js";
// 标记是否是首次加载页面
let isFirstLoad = true;
/**
* 创建通用的设置弹窗
* @param {Object} options 弹窗配置选项
* @param {string} options.title 弹窗标题
* @param {Function} options.renderContent 渲染弹窗内容的函数
* @param {Function} options.onSave 保存按钮点击回调
* @param {Function} options.onCancel 取消按钮点击回调(可选)
* @param {boolean} options.isConfirmDialog 是否是确认对话框(可选)
* @param {string} options.saveButtonText 保存按钮文本(可选)
* @param {string} options.cancelButtonText 取消按钮文本(可选)
* @param {string} options.saveButtonIcon 保存按钮图标(可选)
* @param {boolean} options.disableBackdropAndCloseOnClickOutside 禁用遮罩层和点击外部关闭(可选)
*/
export function createSettingsDialog(options) {
try {
const {
title,
renderContent,
onSave,
onCancel = null,
isConfirmDialog = false,
saveButtonText = '保存',
cancelButtonText = '取消',
saveButtonIcon = 'pi-check',
dialogClassName = null,
disableBackdropAndCloseOnClickOutside = false,
} = options;
let overlay = null;
if (!disableBackdropAndCloseOnClickOutside) {
// 创建遮罩层
overlay = document.createElement('div');
overlay.className = 'settings-modal-overlay';
document.body.appendChild(overlay);
}
// 创建弹窗
const modal = document.createElement('div');
modal.className = 'settings-modal';
// 如果提供了额外的对话框类名,添加到modal
if (dialogClassName) {
modal.classList.add(dialogClassName);
}
// 如果是确认对话框,设置特殊样式
if (isConfirmDialog) {
// 只有在没有提供自定义类名的情况下才设置默认宽度
if (!dialogClassName) {
modal.style.width = 'min(90vw, 400px)';
}
modal.style.minHeight = 'auto';
// 确保确认对话框显示在其他弹窗上方
if (overlay) {
overlay.style.zIndex = 'calc(var(--settings-modal-z-index) + 10)';
}
modal.style.zIndex = 'calc(var(--settings-modal-z-index) + 11)';
}
// 表单修改状态
let isFormModified = false;
// 处理关闭弹窗的逻辑
const handleCloseModal = async (saveAction) => {
// 如果是保存操作,直接保存并关闭,不弹出确认对话框
if (saveAction) {
try {
await onSave(content);
closeModalWithAnimation(modal, overlay);
} catch (error) {
app.extensionManager.toast.add({
severity: "error",
summary: "保存失败",
detail: error.message,
life: 3000
});
}
return;
}
// 只有在表单被修改且不是确认对话框时才显示确认对话框
if (isFormModified && !isConfirmDialog) {
// 创建确认对话框
createSettingsDialog({
title: '确认操作',
isConfirmDialog: true,
saveButtonText: '返回',
saveButtonIcon: 'pi-undo',
cancelButtonText: '关闭',
renderContent: (content) => {
content.style.textAlign = 'center';
content.style.padding = '1rem';
const confirmMessage = document.createElement('p');
confirmMessage.textContent = '配置已修改,是否保存?';
confirmMessage.style.margin = '0';
confirmMessage.style.fontSize = '1rem';
content.appendChild(confirmMessage);
},
onSave: () => {
// 返回按钮只关闭确认对话框,不执行保存操作
// 这里不需要做任何操作,因为默认的对话框关闭逻辑会在点击按钮后执行
},
onCancel: () => {
// 如果定义了onCancel回调,则执行它
if (onCancel) {
onCancel();
}
// 关闭主对话框
closeModalWithAnimation(modal, overlay);
}
});
} else {
// 没有修改或是确认对话框,直接关闭
if (onCancel && !isConfirmDialog) {
onCancel();
}
closeModalWithAnimation(modal, overlay);
}
};
if (!disableBackdropAndCloseOnClickOutside) {
// 点击遮罩层关闭弹窗
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
handleCloseModal(false);
}
});
}
// 创建弹窗头部
const header = document.createElement('div');
header.className = 'p-dialog-header';
const titleSpan = document.createElement('span');
titleSpan.className = 'p-dialog-title';
titleSpan.textContent = title;
const closeButton = document.createElement('button');
closeButton.className = 'p-dialog-header-icon p-dialog-header-close p-link';
closeButton.setAttribute('aria-label', '关闭');
closeButton.innerHTML = '';
closeButton.onclick = () => {
handleCloseModal(false);
};
const headerIcons = document.createElement('div');
headerIcons.className = 'p-dialog-header-icons';
headerIcons.appendChild(closeButton);
header.appendChild(titleSpan);
header.appendChild(headerIcons);
// 创建弹窗内容
const content = document.createElement('div');
content.className = 'p-dialog-content';
// 监听表单变化
const trackFormChanges = (formElement) => {
// 为所有输入元素添加变更监听
const inputs = formElement.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
const originalValue = input.type === 'checkbox' ? input.checked : input.value;
input.addEventListener('change', () => {
const currentValue = input.type === 'checkbox' ? input.checked : input.value;
// 只有当值真正改变时才标记为已修改
if (currentValue !== originalValue) {
isFormModified = true;
}
});
if (input.tagName.toLowerCase() === 'textarea' || input.type === 'text') {
input.addEventListener('input', () => {
const currentValue = input.value;
// 只有当值真正改变时才标记为已修改
if (currentValue !== originalValue) {
isFormModified = true;
}
});
}
});
// 监听自定义下拉框变化
const dropdowns = formElement.querySelectorAll('.p-dropdown');
dropdowns.forEach(dropdown => {
// 存储原始选中值
const hiddenSelect = dropdown.querySelector('select');
const originalValue = hiddenSelect ? hiddenSelect.value : '';
const observer = new MutationObserver(() => {
const currentValue = hiddenSelect ? hiddenSelect.value : '';
// 只有当值真正改变时才标记为已修改
if (currentValue !== originalValue) {
isFormModified = true;
}
});
observer.observe(dropdown, { attributes: true, childList: true, subtree: true });
});
};
// 渲染内容并跟踪变化
renderContent(content);
// 如果不是确认对话框且内容中有表单,添加变更跟踪
if (!isConfirmDialog) {
const forms = content.querySelectorAll('form');
forms.forEach(trackFormChanges);
// 如果没有找到表单,则监视整个内容区域
if (forms.length === 0) {
trackFormChanges(content);
}
}
// 创建弹窗底部
const footer = document.createElement('div');
footer.className = 'p-dialog-footer';
const cancelButton = document.createElement('button');
cancelButton.className = 'p-button p-component p-button-secondary';
cancelButton.innerHTML = `${cancelButtonText}`;
// 为不同类型的对话框设置不同的关闭行为
if (isConfirmDialog) {
cancelButton.onclick = () => {
// 对于确认对话框,“关闭”按钮应执行onCancel回调(该回调负责关闭主窗口),然后关闭自己
if (onCancel) {
onCancel();
}
closeModalWithAnimation(modal, overlay);
};
} else {
cancelButton.onclick = () => {
// 对于普通对话框,使用标准的关闭处理逻辑
handleCloseModal(false);
};
}
const saveButton = document.createElement('button');
saveButton.className = 'p-button p-component';
saveButton.innerHTML = `${saveButtonText}`;
saveButton.onclick = () => {
// 如果是确认对话框中的返回按钮,直接关闭确认对话框
if (isConfirmDialog) {
try {
// 对于确认对话框,先执行onSave回调,然后关闭弹窗
const result = onSave && onSave(content);
// 如果onSave返回Promise,等待其完成
if (result instanceof Promise) {
result.then(() => {
closeModalWithAnimation(modal, overlay);
}).catch(error => {
logger.error(`确认对话框处理失败: ${error.message}`);
closeModalWithAnimation(modal, overlay);
});
} else {
// 普通返回值,直接关闭
closeModalWithAnimation(modal, overlay);
}
} catch (error) {
logger.error(`确认对话框处理失败: ${error.message}`);
closeModalWithAnimation(modal, overlay);
}
} else {
handleCloseModal(true);
}
};
footer.appendChild(cancelButton);
footer.appendChild(saveButton);
// 添加拖动功能
let isDragging = false;
let startX = 0;
let startY = 0;
let offsetX = 0;
let offsetY = 0;
const startDragging = (e) => {
if (e.target === closeButton || closeButton.contains(e.target)) return;
e.preventDefault();
isDragging = true;
// 获取鼠标相对于弹窗的偏移量
const rect = modal.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
// 移除居中定位
modal.style.transform = 'none';
modal.style.left = rect.left + 'px';
modal.style.top = rect.top + 'px';
// 添加拖动状态
modal.classList.add('dragging');
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDragging);
};
const onDrag = (e) => {
if (!isDragging) return;
e.preventDefault();
// 计算新位置(考虑鼠标在弹窗内的偏移量)
let newLeft = e.clientX - offsetX;
let newTop = e.clientY - offsetY;
// 获取视口和弹窗尺寸
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const modalRect = modal.getBoundingClientRect();
const modalWidth = modalRect.width;
const modalHeight = modalRect.height;
// 边界检查(保持10px边距)
const margin = 10;
newLeft = Math.max(margin, Math.min(newLeft, viewportWidth - modalWidth - margin));
newTop = Math.max(margin, Math.min(newTop, viewportHeight - modalHeight - margin));
// 更新位置
modal.style.left = `${newLeft}px`;
modal.style.top = `${newTop}px`;
};
const stopDragging = () => {
isDragging = false;
modal.classList.remove('dragging');
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDragging);
};
header.addEventListener('mousedown', startDragging);
// 组装弹窗
modal.appendChild(header);
modal.appendChild(content);
modal.appendChild(footer);
// 显示弹窗和遮罩层
document.body.appendChild(modal);
// 添加显示动画
requestAnimationFrame(() => {
modal.classList.add('modal-show');
if (overlay) {
overlay.classList.add('overlay-show');
}
});
return modal;
} catch (error) {
logger.error(`创建设置弹窗失败: ${error.message}`);
app.extensionManager.toast.add({
severity: "error",
summary: "创建弹窗失败",
detail: error.message || "创建设置弹窗过程中发生错误",
life: 3000
});
}
}
/**
* 关闭弹窗时添加动画效果
* @param {HTMLElement} modal 弹窗元素
* @param {HTMLElement} overlay 遮罩层元素
*/
export function closeModalWithAnimation(modal, overlay) {
// 添加关闭动画类
modal.classList.remove('modal-show');
modal.classList.add('modal-hide');
if (overlay) {
overlay.classList.remove('overlay-show');
overlay.classList.add('overlay-hide');
}
// 等待动画完成后移除弹窗和遮罩层
setTimeout(() => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
}, 300); // 与 CSS transition 时间相匹配
}
/**
* 创建表单组
* @param {string} title 表单组标题
* @param {Array<{text: string, url: string}>} links 标题右侧的链接数组
* @returns {HTMLElement} 表单组容器
*/
export function createFormGroup(title, links = []) {
const group = document.createElement('div');
group.className = 'settings-form-section';
const titleContainer = document.createElement('div');
titleContainer.className = 'settings-form-section-header';
titleContainer.style.display = 'flex';
titleContainer.style.alignItems = 'center';
titleContainer.style.justifyContent = 'space-between';
titleContainer.style.marginBottom = '10px';
const titleElem = document.createElement('h3');
titleElem.className = 'settings-form-section-title';
titleElem.textContent = title;
titleElem.style.margin = '0';
const linksContainer = document.createElement('div');
linksContainer.style.display = 'flex';
linksContainer.style.gap = '8px';
linksContainer.style.alignItems = 'center';
if (links.length > 0) {
const serviceLinksContainer = document.createElement('div');
serviceLinksContainer.className = 'settings-service-links';
links.forEach((linkInfo, index) => {
if (index > 0) {
// 添加分隔符
const separator = document.createElement('span');
separator.textContent = '|';
separator.className = 'settings-service-separator';
serviceLinksContainer.appendChild(separator);
}
const link = document.createElement('a');
link.href = linkInfo.url;
link.target = '_blank';
link.textContent = linkInfo.text;
link.className = 'settings-service-link';
serviceLinksContainer.appendChild(link);
});
linksContainer.appendChild(serviceLinksContainer);
}
titleContainer.appendChild(titleElem);
titleContainer.appendChild(linksContainer);
group.appendChild(titleContainer);
return group;
}
/**
* 创建输入框组
* @param {string} label 标签文本
* @param {string} placeholder 占位符文本
* @param {string} type 输入框类型
* @returns {Object} 包含 group 和 input 的对象
*/
export function createInputGroup(label, placeholder, type = 'text') {
const group = document.createElement('div');
group.className = 'settings-form-group';
const labelElem = document.createElement('label');
labelElem.className = 'settings-form-label';
labelElem.textContent = label;
const input = document.createElement('input');
input.className = 'p-inputtext p-component flex-1';
input.type = type;
input.placeholder = placeholder;
group.appendChild(labelElem);
group.appendChild(input);
return { group, input };
}
/**
* 创建下拉选择框组
* @param {string} label 标签文本
* @param {Array<{value: string, text: string}>} options 选项列表
* @param {string} [initialValue=null] 初始选中的值
* @returns {Object} 包含 group 和 select 的对象
*/
export function createSelectGroup(label, options, initialValue = null) {
const group = document.createElement('div');
group.className = 'settings-form-group';
const labelElem = document.createElement('label');
labelElem.className = 'settings-form-label';
labelElem.textContent = label;
// Main Container
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'p-dropdown p-component w-full';
dropdownContainer.style.position = 'relative'; // Needed for panel positioning
// Hidden select for form data and accessibility
const select = document.createElement('select');
const hiddenContainer = document.createElement('div');
hiddenContainer.className = 'p-hidden-accessible';
hiddenContainer.appendChild(select);
// Visible Part: Label and Trigger
const dropdownLabel = document.createElement('span');
dropdownLabel.className = 'p-dropdown-label p-inputtext';
const dropdownTrigger = document.createElement('div');
dropdownTrigger.className = 'p-dropdown-trigger';
dropdownTrigger.innerHTML = '';
// Dropdown Panel (the menu)
const dropdownPanel = document.createElement('div');
dropdownPanel.className = 'p-dropdown-panel p-component settings-modal-dropdown-panel';
dropdownPanel.style.display = 'none'; // Initially hidden
dropdownPanel.style.zIndex = '10001'; // 确保层级足够高
const dropdownItemsWrapper = document.createElement('div');
dropdownItemsWrapper.className = 'p-dropdown-items-wrapper';
const dropdownList = document.createElement('ul');
dropdownList.className = 'p-dropdown-items';
dropdownList.setAttribute('role', 'listbox');
// Populate options
options.forEach(opt => {
const optionEl = document.createElement('option');
optionEl.value = opt.value;
optionEl.textContent = opt.text;
select.appendChild(optionEl);
const itemEl = document.createElement('li');
itemEl.className = 'p-dropdown-item';
itemEl.textContent = opt.text;
itemEl.dataset.value = opt.value;
itemEl.setAttribute('role', 'option');
itemEl.addEventListener('click', (e) => {
e.stopPropagation();
// Update highlight
dropdownList.querySelectorAll('.p-dropdown-item').forEach(el => el.classList.remove('p-highlight'));
itemEl.classList.add('p-highlight');
select.value = opt.value;
closePanel();
select.dispatchEvent(new Event('change', { bubbles: true }));
});
dropdownList.appendChild(itemEl);
});
// Set initial display value
const valueToSet = initialValue !== null && options.some(o => o.value === initialValue)
? initialValue
: (options.length > 0 ? options[0].value : null);
if (valueToSet !== null) {
const selectedOption = options.find(o => o.value === valueToSet);
if (selectedOption) {
dropdownLabel.textContent = selectedOption.text;
select.value = selectedOption.value;
// Set initial highlight
const initialItem = dropdownList.querySelector(`.p-dropdown-item[data-value="${select.value}"]`);
if (initialItem) {
initialItem.classList.add('p-highlight');
}
}
} else {
dropdownLabel.textContent = ' '; // Placeholder
}
// Assemble dropdown
dropdownItemsWrapper.appendChild(dropdownList);
dropdownPanel.appendChild(dropdownItemsWrapper);
dropdownContainer.appendChild(hiddenContainer);
dropdownContainer.appendChild(dropdownLabel);
dropdownContainer.appendChild(dropdownTrigger);
group.appendChild(labelElem);
group.appendChild(dropdownContainer);
// --- Event Handling ---
let isOpen = false;
// 更新面板位置的函数
const updatePanelPosition = () => {
if (!isOpen) return;
const rect = dropdownContainer.getBoundingClientRect();
dropdownPanel.style.top = rect.bottom + 'px';
dropdownPanel.style.left = rect.left + 'px';
dropdownPanel.style.width = rect.width + 'px';
};
const closePanel = () => {
if (!isOpen) return;
isOpen = false;
dropdownPanel.classList.add('p-hidden');
dropdownPanel.classList.remove('p-enter-active');
dropdownContainer.classList.remove('p-dropdown-open', 'p-focus');
// 移除事件监听器
window.removeEventListener('resize', updatePanelPosition);
window.removeEventListener('scroll', updatePanelPosition, true);
// 等待动画完成后再移除面板和事件监听
setTimeout(() => {
if (dropdownPanel.parentNode === document.body) {
document.body.removeChild(dropdownPanel);
}
document.removeEventListener('click', handleOutsideClick, true);
}, 120); // 与CSS中的transition时间相匹配
};
const openPanel = () => {
isOpen = true;
// 将面板临时附加到 body 上以避免裁切
document.body.appendChild(dropdownPanel);
// 计算下拉框相对于视口的位置
const rect = dropdownContainer.getBoundingClientRect();
// 设置面板样式
dropdownPanel.style.position = 'fixed';
dropdownPanel.style.display = 'block';
dropdownPanel.style.top = rect.bottom + 'px';
dropdownPanel.style.left = rect.left + 'px';
dropdownPanel.style.width = rect.width + 'px';
dropdownPanel.style.background = 'var(--p-dialog-background)';
dropdownPanel.style.border = '1px solid var(--p-select-overlay-border-color)';
dropdownPanel.style.boxShadow = 'var(--p-select-overlay-shadow)';
dropdownPanel.style.borderRadius = 'var(--p-select-overlay-border-radius)';
dropdownPanel.classList.remove('p-hidden');
// 强制重排,确保动画生效
dropdownPanel.offsetHeight;
dropdownPanel.classList.add('p-enter-active');
dropdownContainer.classList.add('p-dropdown-open', 'p-focus');
document.addEventListener('click', handleOutsideClick, true);
// 添加窗口大小变化和滚动事件监听器,以便重新定位面板
window.addEventListener('resize', updatePanelPosition);
window.addEventListener('scroll', updatePanelPosition, true);
};
const handleOutsideClick = (e) => {
// 如果点击的是下拉框本身,切换状态
if (dropdownContainer.contains(e.target)) {
e.stopPropagation();
if (isOpen) {
closePanel();
} else {
openPanel();
}
return;
}
// 如果点击的是其他区域,关闭面板
closePanel();
};
// 初始化面板状态
dropdownPanel.classList.add('p-hidden');
dropdownContainer.addEventListener('click', handleOutsideClick);
select.addEventListener('change', () => {
const selectedOption = options.find(o => o.value === select.value);
if (selectedOption) {
dropdownLabel.textContent = selectedOption.text;
// Update highlight
dropdownList.querySelectorAll('.p-dropdown-item').forEach(el => {
if (el.dataset.value === select.value) {
el.classList.add('p-highlight');
} else {
el.classList.remove('p-highlight');
}
});
}
});
return { group, select };
}
/**
* 创建水平布局的表单组
* @param {Array<{label: string, element: HTMLElement}>} items 表单项数组
* @returns {HTMLElement} 水平布局的表单组
*/
export function createHorizontalFormGroup(items) {
const group = document.createElement('div');
group.className = 'settings-form-group horizontal';
items.forEach(item => {
const container = document.createElement('div');
const label = document.createElement('label');
label.className = 'settings-form-label';
label.textContent = item.label;
container.appendChild(label);
container.appendChild(item.element);
group.appendChild(container);
});
return group;
}
/**
* 显示API配置弹窗
*/
function showAPIConfigModal() {
try {
// 调用API配置管理器的显示弹窗方法
apiConfigManager.showAPIConfigModal();
} catch (error) {
logger.error(`打开API配置弹窗失败: ${error.message}`);
app.extensionManager.toast.add({
severity: "error",
summary: "打开配置失败",
detail: error.message || "打开配置弹窗过程中发生错误",
life: 3000
});
}
}
/**
* 显示规则配置弹窗
*/
function showRulesConfigModal() {
try {
// 调用规则配置管理器的显示弹窗方法
rulesConfigManager.showRulesConfigModal();
} catch (error) {
logger.error(`打开规则配置弹窗失败: ${error.message}`);
app.extensionManager.toast.add({
severity: "error",
summary: "打开配置失败",
detail: error.message || "打开配置弹窗过程中发生错误",
life: 3000
});
}
}
/**
* 显示标签配置弹窗
*/
function showTagsConfigModal() {
try {
logger.debug('打开标签配置弹窗');
// 使用tagConfigManager实例显示标签配置弹窗
tagConfigManager.showTagsConfigModal();
} catch (error) {
logger.error(`打开标签配置弹窗失败: ${error.message}`);
app.extensionManager.toast.add({
severity: "error",
summary: "打开配置失败",
detail: error.message || "打开配置弹窗过程中发生错误",
life: 3000
});
}
}
// ====================== 设置管理 ======================
/**
* 创建加载按钮
*/
function createLoadingButton(text, onClick, showSuccessToast = true) {
const button = document.createElement('button');
button.className = 'p-button p-component p-button-primary';
button.style.width = '208px'; // 相当于w-52
button.innerHTML = `${text}`;
button.addEventListener('click', async () => {
if (button.disabled) return;
// 开始加载状态
button.disabled = true;
button.classList.add('p-disabled');
try {
await onClick();
// 只有在 showSuccessToast 为 true 时才显示成功提示
if (showSuccessToast) {
app.extensionManager.toast.add({
severity: "success",
summary: "清理已清理完成",
life: 3000
});
}
} catch (error) {
// 显示错误提示
app.extensionManager.toast.add({
severity: "error",
summary: "操作失败",
detail: error.message || "操作过程中发生错误",
life: 3000
});
logger.error(`按钮操作失败: ${error.message}`);
} finally {
// 恢复按钮状态
button.disabled = false;
button.classList.remove('p-disabled');
}
});
return button;
}
/**
* 注册设置选项
* 将设置选项添加到ComfyUI设置面板
*/
export function registerSettings() {
try {
app.registerExtension({
name: "PromptAssistant.Settings",
settings: [
// 总开关 - 独立控制小助手系统级功能
{
id: "PromptAssistant.Features.Enabled",
name: "启用小助手",
category: ["✨提示词小助手", "小助手功能开关", "总开关"],
type: "boolean",
defaultValue: true,
tooltip: "关闭后,提示词小助手所有功能将禁用",
onChange: async (value) => {
try {
// 获取当前状态,用于判断是否是初始化
const currentState = window.FEATURES.enabled;
// 只有状态真正变化时才输出日志
if (currentState !== value) {
logger.log(`总开关状态变更 | 状态:${value ? "启用" : "禁用"}`);
} else {
// 如果状态没有变化,使用调试级别日志
logger.debug(`总开关状态保持不变 | 状态:${value ? "启用" : "禁用"}`);
}
// 更新全局状态
window.FEATURES.enabled = value;
// 从全局 app 对象获取 promptAssistant 实例
const promptAssistantInstance = app.promptAssistant;
const imageCaptionInstance = app.imageCaption;
if (!promptAssistantInstance) {
logger.error("总开关切换失败 | 错误:未找到PromptAssistant实例");
return;
}
// 根据开关状态执行相应操作
if (value) {
// 启用功能
await promptAssistantInstance.toggleGlobalFeature(true, currentState !== value);
if (imageCaptionInstance) {
await imageCaptionInstance.toggleGlobalFeature(true, currentState !== value);
}
// 只在状态真正变化且不是首次加载时记录日志和显示提示
if (currentState !== value) {
logger.debug("功能启用完成");
// 只在状态发生变化且不是首次加载时显示提示
if (!isFirstLoad) {
app.extensionManager.toast.add({
severity: "info",
summary: "提示词小助手已启用",
life: 3000
});
}
}
} else {
// 禁用功能
await promptAssistantInstance.toggleGlobalFeature(false, currentState !== value);
if (imageCaptionInstance) {
await imageCaptionInstance.toggleGlobalFeature(false, currentState !== value);
}
// 只在状态真正变化且不是首次加载时记录日志和显示提示
if (currentState !== value) {
logger.debug("功能禁用完成");
// 只在状态发生变化且不是首次加载时显示提示
if (!isFirstLoad) {
app.extensionManager.toast.add({
severity: "warn",
summary: "提示词小助手已禁用",
life: 3000
});
}
}
}
// 设置首次加载标志为 false,表示已经完成首次加载
isFirstLoad = false;
} catch (error) {
logger.error(`总开关切换异常 | 错误:${error.message}`);
}
}
},
// API 配置按钮
{
id: "PromptAssistant.Features.APIConfig",
name: "百度和大语言模型API配置",
category: ["✨提示词小助手", " 配置", "API配置"],
tooltip: "配置或修改API信息",
type: () => {
const row = document.createElement("tr");
row.className = "promptwidget-settings-row";
const labelCell = document.createElement("td");
labelCell.className = "comfy-menu-label";
row.appendChild(labelCell);
const buttonCell = document.createElement("td");
const button = createLoadingButton("API管理器", async () => {
showAPIConfigModal();
}, false); // 设置 showSuccessToast 为 false
buttonCell.appendChild(button);
row.appendChild(buttonCell);
return row;
}
},
// 历史功能(包含历史、撤销、重做按钮)
{
id: "PromptAssistant.Features.History",
name: "启用历史功能",
category: ["✨提示词小助手", "小助手功能开关", "历史功能"],
type: "boolean",
defaultValue: true,
tooltip: "开启或关闭历史、撤销、重做功能",
onChange: (value) => {
const oldValue = FEATURES.history;
FEATURES.history = value;
handleFeatureChange('历史功能', value, oldValue);
logger.log(`历史功能 - 已${value ? "启用" : "禁用"}`);
}
},
// 标签工具
{
id: "PromptAssistant.Features.Tag",
name: "启用标签工具",
category: ["✨提示词小助手", "小助手功能开关", "标签功能"],
type: "boolean",
defaultValue: true,
tooltip: "开启或关闭标签工具功能",
onChange: (value) => {
const oldValue = FEATURES.tag;
FEATURES.tag = value;
handleFeatureChange('标签工具', value, oldValue);
logger.log(`标签工具 - 已${value ? "启用" : "禁用"}`);
}
},
// 扩写功能
{
id: "PromptAssistant.Features.Expand",
name: "启用扩写功能",
category: ["✨提示词小助手", "小助手功能开关", "扩写功能"],
type: "boolean",
defaultValue: true,
tooltip: "开启或关闭扩写功能",
onChange: (value) => {
const oldValue = FEATURES.expand;
FEATURES.expand = value;
handleFeatureChange('扩写功能', value, oldValue);
logger.log(`扩写功能 - 已${value ? "启用" : "禁用"}`);
}
},
// 翻译功能
{
id: "PromptAssistant.Features.Translate",
name: "启用翻译功能",
category: ["✨提示词小助手", "小助手功能开关", "翻译功能"],
type: "boolean",
defaultValue: true,
tooltip: "开启或关闭翻译功能",
onChange: (value) => {
const oldValue = FEATURES.translate;
FEATURES.translate = value;
handleFeatureChange('翻译功能', value, oldValue);
logger.log(`翻译功能 - 已${value ? "启用" : "禁用"}`);
}
},
// 使用翻译缓存功能
{
id: "PromptAssistant.Features.UseTranslateCache",
name: "使用翻译缓存",
category: ["✨提示词小助手", " 翻译功能设置", "翻译缓存"],
type: "boolean",
defaultValue: true,
tooltip: "开启后,会优先使用之前相同翻译的结果,避免相同内容重复翻译",
onChange: (value) => {
const oldValue = FEATURES.useTranslateCache;
FEATURES.useTranslateCache = value;
logger.log(`使用翻译缓存 - 已${value ? "启用" : "禁用"}`);
}
},
// 自动翻译功能 - 暂时禁用
/*
{
id: "PromptAssistant.Features.AutoTranslate",
name: "自动翻译",
category: ["✨提示词小助手", " 翻译功能设置", "自动翻译"],
type: "boolean",
defaultValue: false,
tooltip: "开启后,工作流运行时将自动翻译CLIP节点的中文文本,前端显示保持不变",
onChange: (value) => {
const oldValue = FEATURES.autoTranslate;
FEATURES.autoTranslate = value;
handleFeatureChange('自动翻译', value, oldValue);
updateAutoTranslateIndicators(value);
logger.log(`自动翻译 - 已${value ? "启用" : "禁用"}`);
}
},
*/
// 图像反推功能
{
id: "PromptAssistant.Features.ImageCaption",
name: "启用图像反推功能",
category: ["✨提示词小助手", "小助手功能开关", "图像反推"],
type: "boolean",
defaultValue: true,
tooltip: "开启或关闭图像反推提示词功能",
onChange: (value) => {
const oldValue = FEATURES.imageCaption;
FEATURES.imageCaption = value;
handleFeatureChange('图像反推', value, oldValue);
logger.log(`图像反推功能 - 已${value ? "启用" : "禁用"}`);
}
},
// 系统设置
{
id: "PromptAssistant.Settings.LogLevel",
name: "日志级别",
category: ["✨提示词小助手", "系统", "日志级别"],
type: "combo",
defaultValue: "0",
options: [
{ text: "错误日志", value: "0" },
{ text: "基础日志", value: "1" },
{ text: "详细日志", value: "2" }
],
tooltip: "设置日志输出级别:错误日志(仅错误)、基础日志(错误+基础信息)、详细日志(错误+基础信息+调试信息)",
onChange: (value) => {
const oldValue = window.FEATURES.logLevel;
window.FEATURES.logLevel = parseInt(value);
logger.setLevel(window.FEATURES.logLevel);
logger.log(`日志级别已更新 | 原级别:${oldValue} | 新级别:${value}`);
}
},
{
id: "PromptAssistant.Settings.IconOpacity",
name: " 小助手图标不透明度",
category: ["✨提示词小助手", "界面", "小助手图标"],
type: "slider",
min: 0,
max: 100,
step: 1,
defaultValue: 30,
tooltip: "设置折叠后小助手图标的不透明度",
onChange: (value) => {
// 将0-100的值转换为0-1的透明度
const opacity = value * 0.01;
document.documentElement.style.setProperty('--assistant-icon-opacity', opacity);
logger.log(`小助手图标不透明度已更新 | 值:${value}% | 透明度:${opacity}`);
},
onLoad: (value) => {
// 初始化时应用默认值
const opacity = value * 0.01;
document.documentElement.style.setProperty('--assistant-icon-opacity', opacity);
logger.debug(`小助手图标不透明度初始化 | 值:${value}% | 透明度:${opacity}`);
}
},
{
id: "PromptAssistant.Settings.ClearCache",
name: "清理历史、标签、翻译缓存",
category: ["✨提示词小助手", "系统", "清理缓存"],
tooltip: "清理所有缓存,包括历史记录、标签、翻译缓存",
type: () => {
const row = document.createElement("tr");
row.className = "promptwidget-settings-row";
const labelCell = document.createElement("td");
labelCell.className = "comfy-menu-label";
row.appendChild(labelCell);
const buttonCell = document.createElement("td");
const button = createLoadingButton("清理所有缓存", async () => {
try {
// 获取清理前的缓存统计
const beforeStats = {
history: HistoryCacheService.getHistoryStats(),
tags: 0,
translate: TranslateCacheService.getTranslateCacheStats()
};
// 统计所有标签数量
const tagCacheKeys = Object.keys(localStorage)
.filter(key => key.startsWith(CACHE_CONFIG.TAG_KEY_PREFIX));
// 计算所有缓存中的标签总数
tagCacheKeys.forEach(key => {
try {
const cacheData = JSON.parse(localStorage.getItem(key));
if (cacheData && typeof cacheData === 'object') {
// 获取缓存中的标签数量
const tagCount = Object.keys(cacheData).length;
beforeStats.tags += tagCount;
}
} catch (e) {
// 移除错误日志,静默处理解析错误
}
});
// 执行历史记录清理操作
HistoryCacheService.clearAllHistory();
// 清理所有标签缓存
TagCacheService.clearAllTagCache();
// 清理翻译缓存
TranslateCacheService.clearAllTranslateCache();
// 清理旧版本的标签缓存(以PromptAssistant_tag_cache_开头的所有记录)
Object.keys(localStorage)
.filter(key => key.startsWith('PromptAssistant_tag_cache_'))
.forEach(key => localStorage.removeItem(key));
// 清除1.0.3以前版本遗留的三项配置信息,避免泄露
localStorage.removeItem("PromptAssistant_Settings_llm_api_key");
localStorage.removeItem("PromptAssistant_Settings_baidu_translate_secret");
localStorage.removeItem("PromptAssistant_Settings_baidu_translate_appid");
// 获取清理后的缓存统计
const afterStats = {
history: HistoryCacheService.getHistoryStats(),
tags: 0, // 清理后标签数应该为0
translate: TranslateCacheService.getTranslateCacheStats()
};
// 计算清理数量
const clearedHistory = beforeStats.history.total - afterStats.history.total;
const clearedTags = beforeStats.tags;
const clearedTranslate = beforeStats.translate.total - afterStats.translate.total;
// 只输出最终统计结果
logger.log(`缓存清理完成 | 历史记录: ${clearedHistory}条 | 标签: ${clearedTags}个 | 翻译: ${clearedTranslate}条`);
// 更新所有实例的撤销/重做按钮状态
PromptAssistant.instances.forEach((instance) => {
if (instance && instance.nodeId && instance.inputId) {
UIToolkit.updateUndoRedoButtonState(instance, HistoryCacheService);
}
});
} catch (error) {
// 简化错误日志
logger.error(`缓存清理失败`);
throw error;
}
});
buttonCell.appendChild(button);
row.appendChild(buttonCell);
return row;
}
},
// 翻译方法选择
{
id: "PromptAssistant.Settings.TranslateType",
name: "翻译方法",
category: ["✨提示词小助手", " 翻译功能设置", "翻译方法"],
type: "combo",
defaultValue: "baidu",
options: [
{ text: "百度翻译", value: "baidu" },
{ text: "大语言模型翻译", value: "llm" }
],
tooltip: "可选百度机翻或者大语言模型翻译,注意:大语言模型翻译速度比较慢,格式可能会发生变化。",
onChange: (value) => {
// 直接修改 app.settings 对象
if (!app.settings) {
app.settings = {};
}
app.settings["PromptAssistant.Settings.TranslateType"] = value;
logger.debug("翻译方式已更新:" + value);
}
},
// 关于插件信息
{
id: "PromptAssistant.Settings.About",
name: "关于",
category: ["✨提示词小助手", " ✨提示词小助手"],
type: () => {
const row = document.createElement("tr");
row.className = "promptwidget-settings-row";
const cell = document.createElement("td");
cell.colSpan = 2;
cell.style.display = "flex";
cell.style.alignItems = "center";
cell.style.gap = "12px";
// 版本徽标
const versionBadge = document.createElement("img");
versionBadge.alt = "Version";
versionBadge.style.display = "block";
versionBadge.style.height = "20px";
// 从全局变量获取版本号
if (!window.PromptAssistant_Version) {
logger.error("未找到版本号,徽标将无法正确显示");
versionBadge.src = `https://img.shields.io/badge/%E7%89%88%E6%9C%AC-%E6%9C%AA%E7%9F%A5-red?style=flat`;
} else {
const version = window.PromptAssistant_Version;
versionBadge.src = `https://img.shields.io/badge/%E7%89%88%E6%9C%AC-${version}-green?style=flat`;
logger.debug(`版本号徽标已更新: ${version}`);
}
cell.appendChild(versionBadge);
// GitHub 徽标
const authorTag = document.createElement("a");
authorTag.href = "https://github.com/yawiii/comfyui_prompt_assistant";
authorTag.target = "_blank";
authorTag.style.textDecoration = "none";
authorTag.style.display = "flex";
authorTag.style.alignItems = "center";
const authorBadge = document.createElement("img");
authorBadge.alt = "Static Badge";
authorBadge.src = "https://img.shields.io/badge/Github-Yawiii-blue?style=flat&logo=github&logoColor=black&labelColor=%23FFFFFF&color=%2307A3D7";
authorBadge.style.display = "block";
authorBadge.style.height = "20px";
authorTag.appendChild(authorBadge);
cell.appendChild(authorTag);
// B站徽标
const biliTag = document.createElement("a");
biliTag.href = "https://space.bilibili.com/520680644";
biliTag.target = "_blank";
biliTag.style.textDecoration = "none";
biliTag.style.display = "flex";
biliTag.style.alignItems = "center";
const biliBadge = document.createElement("img");
biliBadge.alt = "Bilibili";
biliBadge.src = "https://img.shields.io/badge/B%E7%AB%99-%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B-blue?style=flat&logo=bilibili&logoColor=2300A5DC&labelColor=%23FFFFFF&color=%2307A3D7";
biliBadge.style.display = "block";
biliBadge.style.height = "20px";
biliTag.appendChild(biliBadge);
cell.appendChild(biliTag);
// // 反馈群徽标
// const wechatTag = document.createElement("a");
// wechatTag.href = "https://img.shields.io/badge/Bug-%E5%8F%8D%E9%A6%88%E4%BA%A4%E6%B5%81-white?logo=wechat&logoColor=green";
// wechatTag.target = "_blank";
// wechatTag.style.textDecoration = "none";
// wechatTag.style.display = "flex";
// wechatTag.style.alignItems = "center";
// const wechatBadge = document.createElement("img");
// wechatBadge.alt = "Bug反馈交流";
// wechatBadge.src = "https://img.shields.io/badge/Bug-%E5%8F%8D%E9%A6%88%E4%BA%A4%E6%B5%81-blue?logo=wechat&logoColor=green&labelColor=%23FFFFFF&color=%2307A3D7";
// wechatBadge.style.display = "block";
// wechatBadge.style.height = "20px";
// wechatTag.appendChild(wechatBadge);
// cell.appendChild(wechatTag);
row.appendChild(cell);
return row;
}
},
// 规则配置按钮
{
id: "PromptAssistant.Features.RulesConfig",
name: "扩写和反推规则修改",
category: ["✨提示词小助手", " 配置", "规则"],
tooltip: "可以自定义扩写规则,和反推提示词规则,使得提示词生成更加符合你的需求",
type: () => {
const row = document.createElement("tr");
row.className = "promptwidget-settings-row";
const labelCell = document.createElement("td");
labelCell.className = "comfy-menu-label";
row.appendChild(labelCell);
const buttonCell = document.createElement("td");
const button = createLoadingButton("规则管理器", async () => {
showRulesConfigModal();
}, false);
buttonCell.appendChild(button);
row.appendChild(buttonCell);
return row;
}
},
// 标签配置按钮
{
id: "PromptAssistant.Features.TagsConfig",
name: "自定义和标签管理",
category: ["✨提示词小助手", " 配置", "标签管理"],
type: () => {
const row = document.createElement("tr");
row.className = "promptwidget-settings-row";
const labelCell = document.createElement("td");
labelCell.className = "comfy-menu-label";
row.appendChild(labelCell);
const buttonCell = document.createElement("td");
const button = createLoadingButton("标签管理器", async () => {
showTagsConfigModal();
}, false);
buttonCell.appendChild(button);
row.appendChild(buttonCell);
return row;
}
}
]
});
logger.log("小助手设置注册成功");
return true;
} catch (error) {
logger.error(`小助手设置注册失败: ${error.message}`);
return false;
}
}