Files
Dontdrunk-ComfyUI-DD-Nodes/extensions/Prompt_Manager/PromptManagerCore.js
T
2025-06-23 15:23:47 +08:00

703 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 提示词管理器核心模块,只包含业务逻辑和数据管理
import { PromptManagerUI } from './styles/PromptManagerUI.js';
import { globalTagColorManager } from './styles/components/TagColorManager.js';
export class PromptManager {
constructor() {
this.prompts = [];
this.loadPrompts();
} async loadPrompts() {
try {
// 从后端API加载最新数据
const response = await fetch('/dd_nodes/load_prompts');
if (response.ok) {
const result = await response.json();
if (result.success && result.data && result.data.prompts) {
this.prompts = result.data.prompts;
// 确保所有提示词都有标签属性(向后兼容)
this.prompts.forEach(prompt => {
if (!prompt.tags) {
prompt.tags = [];
}
});
// 加载标签颜色数据(如果存在)
if (result.data.tags && typeof result.data.tags === 'object') {
console.log('从后端加载标签颜色数据:', result.data.tags);
// 恢复标签颜色到全局标签颜色管理器
Object.entries(result.data.tags).forEach(([tagName, color]) => {
globalTagColorManager.setTagColor(tagName, color);
});
}
console.log(`从后端API加载了 ${this.prompts.length} 个提示词`);
return;
}
}
// 如果API失败,使用空列表
this.prompts = [];
console.log('初始化空的提示词列表');
} catch (error) {
console.error('加载提示词失败:', error);
this.prompts = [];
}
}savePrompts() {
try {
// 直接同步到后端API
this.syncToBackend();
console.log('提示词已自动同步到后端');
} catch (error) {
console.error('保存提示词失败:', error);
}
} // 同步数据到后端API
async syncToBackend() {
try {
// 获取标签数据 - 直接从标签颜色管理器获取最新数据
const tagsData = Object.fromEntries(globalTagColorManager.colorMap);
console.log('同步标签数据到后端:', tagsData);
const response = await fetch('/dd_nodes/save_prompts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompts: this.prompts,
tags: tagsData // 包含标签数据
})
});
if (response.ok) {
const result = await response.json();
if (result.success) {
console.log(`成功同步 ${result.count} 个提示词和 ${Object.keys(tagsData).length} 个标签到后端`);
} else {
console.error('后端同步失败:', result.error);
}
} else {
console.error('后端同步请求失败:', response.status);
}
} catch (error) {
console.error('后端同步失败:', error);
}
}// 已弃用:自动同步功能已转移到后端API
// 保留此方法仅为向后兼容
autoSyncToJsonFile() {
// 现在通过syncToBackend()方法同步到后端
console.log('已转为使用后端API同步');
}// 保存提示词到插件目录的JSON文件
async saveToJsonFile() {
try {
const promptsData = {
version: "2.4.0",
exportTime: new Date().toISOString(),
description: "ComfyUI-DD-Nodes 提示词管理器自动生成的提示词数据文件",
totalCount: this.prompts.length,
prompts: this.prompts
};
const dataStr = JSON.stringify(promptsData, null, 2);
// 由于浏览器安全限制,无法直接写入本地文件
// 但我们可以通过定期提醒用户备份的方式来实现数据持久化
// 检查是否需要提醒用户备份
this.checkBackupReminder();
// 将数据存储在内存中,供导出使用
this._lastSavedData = promptsData;
console.log('提示词数据已准备好,建议定期导出备份');
} catch (error) {
console.warn('准备备份数据失败:', error);
}
}
// 检查备份提醒
checkBackupReminder() {
const lastBackupTime = localStorage.getItem('comfyui_dd_last_backup_time');
const currentTime = Date.now();
const backupInterval = 24 * 60 * 60 * 1000; // 24小时
if (!lastBackupTime || (currentTime - parseInt(lastBackupTime)) > backupInterval) {
// 显示备份提醒
this.showBackupNotification();
localStorage.setItem('comfyui_dd_last_backup_time', currentTime.toString());
}
}
// 显示备份通知
showBackupNotification() {
if (this.prompts.length > 0) {
console.log(`🔔 备份提醒: 您有 ${this.prompts.length} 个提示词,建议导出备份以防数据丢失`);
// 如果有UI实例,可以显示更友好的提醒
if (typeof window !== 'undefined' && this.prompts.length >= 5) {
setTimeout(() => {
if (confirm(`您已创建了 ${this.prompts.length} 个提示词,建议现在导出备份文件吗?\n\n备份文件可以在更新插件或更换设备时恢复您的提示词。`)) {
this.exportPrompts();
}
}, 1000);
}
}
}
// 自动创建本地备份文件(供开发者在插件更新时使用)
createLocalBackup() {
try {
const promptsData = {
version: "2.4.0",
exportTime: new Date().toISOString(),
description: "ComfyUI-DD-Nodes 提示词管理器自动备份",
totalCount: this.prompts.length,
prompts: this.prompts
};
// 将备份数据存储到特殊的localStorage键中
localStorage.setItem('comfyui_dd_prompts_backup', JSON.stringify(promptsData));
localStorage.setItem('comfyui_dd_backup_timestamp', Date.now().toString());
console.log('已创建本地备份');
} catch (error) {
console.error('创建本地备份失败:', error);
}
}
addPrompt(name, content, tags = []) {
const prompt = {
id: Date.now(),
name: name,
content: content,
tags: tags || [],
createdAt: new Date().toISOString()
};
this.prompts.push(prompt);
this.savePrompts();
return prompt;
}
updatePrompt(id, name, content, tags = []) {
const index = this.prompts.findIndex(p => p.id === id);
if (index !== -1) {
this.prompts[index].name = name;
this.prompts[index].content = content;
this.prompts[index].tags = tags || [];
this.prompts[index].updatedAt = new Date().toISOString();
this.savePrompts();
return this.prompts[index];
}
return null;
}
deletePrompt(id) {
const index = this.prompts.findIndex(p => p.id === id);
if (index !== -1) {
this.prompts.splice(index, 1);
this.savePrompts();
return true;
}
return false;
}
getPrompts() {
return this.prompts;
}
// 搜索提示词
searchPrompts(keyword) {
if (!keyword || keyword.trim() === '') {
return this.prompts;
}
const searchTerm = keyword.trim().toLowerCase();
// 检查是否是多标签搜索(以逗号分隔)
const isMultiTagSearch = searchTerm.includes(',');
const searchTags = isMultiTagSearch
? searchTerm.split(',').map(tag => tag.trim()).filter(tag => tag)
: [];
// 按相关性排序搜索结果
const searchResults = this.prompts.map(prompt => {
const name = prompt.name.toLowerCase();
const content = prompt.content.toLowerCase();
const tags = (prompt.tags || []).map(tag => tag.toLowerCase());
let score = 0;
// 如果是多标签搜索
if (isMultiTagSearch && searchTags.length > 0) {
const matchedTags = searchTags.filter(searchTag =>
tags.some(tag => tag.includes(searchTag))
);
if (matchedTags.length > 0) {
// 根据匹配的标签数量给分
score = 1000 + (matchedTags.length * 100);
// 如果所有搜索的标签都匹配,额外加分
if (matchedTags.length === searchTags.length) {
score += 500;
}
}
} else {
// 单关键词搜索
// 标签完全匹配得分最高
const exactTagMatch = tags.find(tag => tag === searchTerm);
if (exactTagMatch) {
score = 1100;
}
// 标签包含搜索词
else if (tags.some(tag => tag.includes(searchTerm))) {
score = 1000;
}
// 名称完全匹配得分次高
else if (name === searchTerm) {
score = 900;
}
// 名称开头匹配得分次高
else if (name.startsWith(searchTerm)) {
score = 800;
}
// 名称包含搜索词
else if (name.includes(searchTerm)) {
score = 700;
}
// 内容开头匹配
else if (content.startsWith(searchTerm)) {
score = 600;
}
// 内容包含搜索词
else if (content.includes(searchTerm)) {
score = 500;
}
// 根据匹配位置调整分数(越靠前分数越高)
if (score > 0 && score < 1000) { // 不调整标签匹配的分数
const nameIndex = name.indexOf(searchTerm);
const contentIndex = content.indexOf(searchTerm);
if (nameIndex >= 0) {
score += (100 - nameIndex); // 名称中越靠前分数越高
} else if (contentIndex >= 0) {
score += (50 - Math.min(contentIndex, 50)); // 内容中越靠前分数越高
}
}
}
return { prompt, score };
})
.filter(item => item.score > 0) // 只返回有匹配的结果
.sort((a, b) => b.score - a.score) // 按分数降序排列
.map(item => item.prompt); // 提取提示词对象
return searchResults;
} // 导出提示词到 JSON 格式
exportPrompts() {
// 获取标签颜色数据
const tagsData = Object.fromEntries(globalTagColorManager.colorMap);
// 使用当前最新的提示词数据
const promptsData = {
version: "2.4.0",
exportTime: new Date().toISOString(),
description: "ComfyUI-DD-Nodes 提示词管理器导出的提示词数据",
totalCount: this.prompts.length,
prompts: this.prompts,
tags: tagsData // 添加标签颜色数据
};
const dataStr = JSON.stringify(promptsData, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
// 使用时间戳生成文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
link.download = `comfyui_prompts_${timestamp}.json`;
link.click();
URL.revokeObjectURL(url);
}// 从 JSON 文件导入提示词(兼容新旧格式)
importPrompts(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => { try {
const imported = JSON.parse(e.target.result);
let promptsToImport = [];
// 检查是否是新格式(包含metadata)
if (imported.prompts && Array.isArray(imported.prompts)) {
promptsToImport = imported.prompts;
// 导入标签颜色数据(如果存在)
if (imported.tags && typeof imported.tags === 'object') {
console.log('导入标签颜色数据:', imported.tags);
// 合并标签颜色到全局标签颜色管理器
Object.entries(imported.tags).forEach(([tagName, color]) => {
globalTagColorManager.setTagColor(tagName, color);
});
}
}
// 兼容旧格式(直接是数组)
else if (Array.isArray(imported)) {
promptsToImport = imported;
}
else {
reject('无效的JSON格式:文件必须包含提示词数组');
return;
}
if (promptsToImport.length === 0) {
reject('文件中没有找到提示词数据');
return;
}
// 合并导入的提示词,避免ID冲突
const maxId = this.prompts.length > 0 ? Math.max(...this.prompts.map(p => p.id)) : 0;
let importedCount = 0;
promptsToImport.forEach((prompt, index) => {
// 验证提示词数据完整性
if (!prompt.name || !prompt.content) {
console.warn(`跳过无效提示词 #${index + 1}:`, prompt);
return;
}
// 检查是否已存在相同名称的提示词
const existingPrompt = this.prompts.find(p => p.name === prompt.name);
if (existingPrompt) {
console.warn(`跳过重复提示词: ${prompt.name}`);
return;
}
// 分配新ID并添加到列表
prompt.id = maxId + importedCount + 1;
// 确保标签属性存在(向后兼容)
if (!prompt.tags) {
prompt.tags = [];
}
// 添加导入时间标记
prompt.importedAt = new Date().toISOString();
this.prompts.push(prompt);
importedCount++;
});
if (importedCount === 0) {
reject('没有导入任何新的提示词(可能都已存在)');
return;
}
// 导入完成后自动保存
this.savePrompts();
resolve(importedCount);
} catch (error) {
reject('解析JSON文件失败: ' + error.message);
}
};
reader.readAsText(file);
});
}
}
export class PromptEmbedder {
constructor(node, promptManager) {
this.node = node;
this.promptManager = promptManager;
this.ui = new PromptManagerUI(this);
}
show() {
this.ui.show();
}
hide() {
this.ui.hide();
}
// 应用提示词到节点
applyPrompt(prompt) {
// 查找所有文本widget
const textWidgets = this.node.widgets.filter(w => this.isTextWidget(w));
if (textWidgets.length === 0) {
alert('未找到可用的文本输入框');
return;
}
// 如果有多个文本输入框,显示选择器
if (textWidgets.length > 1) {
// 首先检查是否有当前聚焦的widget
const focusedWidget = textWidgets.find(w =>
w.element && w.element === document.activeElement
);
if (focusedWidget) {
// 如果有聚焦的widget,直接应用到该widget
this.applyPromptToWidget(prompt, focusedWidget);
return;
}
// 否则显示选择器
this.ui.showTextWidgetSelector(prompt);
return;
}
// 如果只有一个文本输入框,直接应用
this.applyPromptToWidget(prompt, textWidgets[0]);
}
isTextWidget(widget) {
if (!widget) return false;
// 检查widget类型
if (widget.type === 'text' ||
widget.type === 'string' ||
widget.type === 'customtext') {
return true;
}
// 检查widget名称
const textWidgetNames = [
'text', 'prompt', 'positive', 'negative',
'text_g', 'text_l', 'clip_l', 'clip_g', 't5xxl',
'input', 'content', 'description'
];
if (textWidgetNames.includes(widget.name)) {
return true;
}
// 检查DOM元素
if (widget.element) {
const tagName = widget.element.tagName;
if (tagName === 'TEXTAREA') {
return true;
}
if (tagName === 'INPUT' &&
(widget.element.type === 'text' || widget.element.type === 'textarea')) {
return true;
}
}
// 检查是否是ComfyUI的多行文本输入
if (widget.inputEl && widget.inputEl.tagName === 'TEXTAREA') {
return true;
}
// 检查ComfyUI特殊情况
if (widget.options && widget.options.multiline) {
return true;
}
return false;
} applyPromptToWidget(prompt, widget, insertMode = null) {
if (!widget) return;
const currentValue = widget.value || '';
const hasContent = currentValue.trim().length > 0;
// 如果指定了插入模式,直接使用
if (insertMode) {
let newValue;
if (insertMode === 'insert') {
// 插入到选中位置或开头
newValue = this.insertPromptAtCursor(widget, prompt.content);
} else if (insertMode === 'append') {
// 追加到内容末尾
newValue = currentValue + (currentValue ? ', ' : '') + prompt.content;
} else if (insertMode === 'replace') {
// 替换全部内容
newValue = prompt.content;
}
if (newValue !== undefined) {
this.setWidgetValue(widget, newValue);
}
return;
}
// 如果没有指定插入模式且有内容,显示对话框
if (hasContent) {
// 显示自定义确认对话框
this.ui.showInsertDialog(
'当前输入框已有内容',
'是否插入提示词?',
(action) => {
let newValue;
if (action === 'insert') {
// 插入到选中位置或开头
newValue = this.insertPromptAtCursor(widget, prompt.content);
} else if (action === 'append') {
// 追加到内容末尾
newValue = currentValue + (currentValue ? ', ' : '') + prompt.content;
} else if (action === 'replace') {
// 替换全部内容
newValue = prompt.content;
}
if (newValue !== undefined) {
this.setWidgetValue(widget, newValue);
}
}
);
} else {
this.setWidgetValue(widget, prompt.content);
}
}insertPromptAtCursor(widget, promptText) {
const currentValue = widget.value || '';
let cursorPosition = 0;
// 尝试获取文本框的选择位置
if (widget.inputEl && typeof widget.inputEl.selectionStart === 'number') {
// ComfyUI多行文本框
cursorPosition = widget.inputEl.selectionStart;
} else if (widget.element && typeof widget.element.selectionStart === 'number') {
// 普通输入框
cursorPosition = widget.element.selectionStart;
} else {
// 无法获取光标位置,默认插入到开头
cursorPosition = 0;
}
// 在光标位置插入提示词
const beforeCursor = currentValue.substring(0, cursorPosition);
const afterCursor = currentValue.substring(cursorPosition);
// 智能添加分隔符
let insertText = promptText;
// 检查光标前的情况
if (beforeCursor.length > 0) {
// 获取光标前最后一个字符
const lastChar = beforeCursor.charAt(beforeCursor.length - 1);
// 如果前面是换行符,不需要添加逗号
if (lastChar === '\n' || lastChar === '\r') {
// 在新行开始,不添加分隔符
} else {
// 检查是否需要添加逗号分隔符
const beforeTrim = beforeCursor.trimEnd();
if (beforeTrim.length > 0 && !beforeTrim.endsWith(',')) {
insertText = ', ' + insertText;
} else if (beforeCursor.endsWith(' ') && beforeTrim.endsWith(',')) {
// 如果前面已经有逗号和空格,直接插入
insertText = insertText;
} else if (beforeTrim.endsWith(',')) {
// 如果前面有逗号但没有空格
insertText = ' ' + insertText;
}
}
}
// 检查光标后的情况
if (afterCursor.length > 0) {
// 获取光标后第一个字符
const firstChar = afterCursor.charAt(0);
// 如果后面是换行符,不需要添加逗号
if (firstChar === '\n' || firstChar === '\r') {
// 后面是新行,不添加分隔符
} else {
// 检查是否需要添加逗号分隔符
const afterTrim = afterCursor.trimStart();
if (afterTrim.length > 0 && !afterTrim.startsWith(',')) {
insertText = insertText + ', ';
}
}
}
const newValue = beforeCursor + insertText + afterCursor;
// 设置新的光标位置(在插入文本之后)
const newCursorPosition = beforeCursor.length + insertText.length;
// 延迟设置光标位置
setTimeout(() => {
if (widget.inputEl && typeof widget.inputEl.setSelectionRange === 'function') {
widget.inputEl.focus();
widget.inputEl.setSelectionRange(newCursorPosition, newCursorPosition);
} else if (widget.element && typeof widget.element.setSelectionRange === 'function') {
widget.element.focus();
widget.element.setSelectionRange(newCursorPosition, newCursorPosition);
}
}, 50);
return newValue;
}
setWidgetValue(widget, newValue) {
// 设置新值
widget.value = newValue;
// 尝试多种方式触发更新
// 1. 触发widget的回调
if (widget.callback) {
widget.callback(newValue);
}
// 2. 如果有DOM元素,更新DOM并触发事件
if (widget.element) {
widget.element.value = newValue;
widget.element.dispatchEvent(new Event('input', { bubbles: true }));
widget.element.dispatchEvent(new Event('change', { bubbles: true }));
}
// 3. 如果有inputEl(ComfyUI的多行文本输入)
if (widget.inputEl) {
widget.inputEl.value = newValue;
widget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
widget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
// 4. 强制触发ComfyUI的更新机制
if (this.node.onWidgetChanged) {
this.node.onWidgetChanged(widget.name, newValue, null, widget);
}
// 5. 标记节点为脏状态,强制重绘
if (this.node.setDirtyCanvas) {
this.node.setDirtyCanvas(true, true);
}
// 6. 触发图形更新
if (this.node.graph && this.node.graph.change) {
this.node.graph.change();
}
console.log(`已将提示词应用到 ${widget.name}: ${newValue}`);
this.hide();
}
getWidgetDisplayName(widget) {
const nameMap = {
'text': '文本',
'prompt': '提示词',
'positive': '正面提示词',
'negative': '负面提示词',
'text_g': '全局文本',
'text_l': '局部文本',
'clip_l': 'CLIP-L',
'clip_g': 'CLIP-G',
't5xxl': 'T5-XXL'
};
return nameMap[widget.name] || widget.name || '文本输入';
}
getAllTextWidgets() {
return this.node.widgets.filter(w => this.isTextWidget(w));
}
}
export const DEFAULT_CONFIG = {
autoSave: true,
maxPrompts: 1000,
searchDelay: 300
};