1831 lines
73 KiB
JavaScript
1831 lines
73 KiB
JavaScript
/**
|
||
* 规则配置管理器
|
||
* 负责管理规则规则配置弹窗和相关功能
|
||
*/
|
||
|
||
import { app } from "../../../../scripts/app.js";
|
||
import { logger } from '../utils/logger.js';
|
||
import { ResourceManager } from "../utils/resourceManager.js";
|
||
import {
|
||
createSettingsDialog,
|
||
createFormGroup,
|
||
createInputGroup,
|
||
createSelectGroup,
|
||
createTextareaGroup,
|
||
createComboBoxGroup,
|
||
createSelectButtonGroup
|
||
} from "./uiComponents.js";
|
||
import { APIService } from "../services/api.js";
|
||
import { tUI } from "../utils/uiI18n.js";
|
||
|
||
|
||
|
||
class RulesConfigManager {
|
||
constructor() {
|
||
this.systemPrompts = null;
|
||
// ---存储规则列表数据---
|
||
this.expandPrompts = [];
|
||
this.zhVisionPrompts = [];
|
||
this.enVisionPrompts = [];
|
||
this.videoPrompts = [];
|
||
// ---当前激活的规则ID---
|
||
this.activeExpandPromptId = null;
|
||
this.activeZhVisionPromptId = null;
|
||
this.activeEnVisionPromptId = null;
|
||
this.activeVideoPromptId = null;
|
||
|
||
this.isDirty = false;
|
||
this.initialState = null; // 用于存储状态备份
|
||
}
|
||
|
||
/**
|
||
* 备份当前状态
|
||
*/
|
||
_backupState() {
|
||
this.initialState = {
|
||
expandPrompts: JSON.parse(JSON.stringify(this.expandPrompts)),
|
||
translatePrompts: JSON.parse(JSON.stringify(this.translatePrompts || [])),
|
||
zhVisionPrompts: JSON.parse(JSON.stringify(this.zhVisionPrompts)),
|
||
enVisionPrompts: JSON.parse(JSON.stringify(this.enVisionPrompts)),
|
||
videoPrompts: JSON.parse(JSON.stringify(this.videoPrompts || [])),
|
||
activeExpandPromptId: this.activeExpandPromptId,
|
||
activeZhVisionPromptId: this.activeZhVisionPromptId,
|
||
activeEnVisionPromptId: this.activeEnVisionPromptId,
|
||
activeVideoPromptId: this.activeVideoPromptId,
|
||
};
|
||
this.isDirty = false;
|
||
// 标记表单未修改
|
||
const form = document.querySelector('.rules-config-dialog .rules-config-form');
|
||
if (form && form.elements.modified_marker) {
|
||
form.elements.modified_marker.value = 'initial';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从备份恢复状态
|
||
*/
|
||
_restoreState() {
|
||
if (this.initialState) {
|
||
this.expandPrompts = JSON.parse(JSON.stringify(this.initialState.expandPrompts));
|
||
this.translatePrompts = JSON.parse(JSON.stringify(this.initialState.translatePrompts || []));
|
||
this.zhVisionPrompts = JSON.parse(JSON.stringify(this.initialState.zhVisionPrompts));
|
||
this.enVisionPrompts = JSON.parse(JSON.stringify(this.initialState.enVisionPrompts));
|
||
this.videoPrompts = JSON.parse(JSON.stringify(this.initialState.videoPrompts || []));
|
||
this.activeExpandPromptId = this.initialState.activeExpandPromptId;
|
||
this.activeZhVisionPromptId = this.initialState.activeZhVisionPromptId;
|
||
this.activeEnVisionPromptId = this.initialState.activeEnVisionPromptId;
|
||
this.activeVideoPromptId = this.initialState.activeVideoPromptId;
|
||
}
|
||
this.isDirty = false;
|
||
}
|
||
|
||
|
||
/**
|
||
* 标记配置已修改
|
||
*/
|
||
_setDirty() {
|
||
this.isDirty = true;
|
||
// 触发表单修改,以便通用对话框检测到未保存的更改
|
||
const form = document.querySelector('.rules-config-dialog .rules-config-form');
|
||
if (form && form.elements.modified_marker) {
|
||
form.elements.modified_marker.value = 'modified_' + Date.now();
|
||
form.elements.modified_marker.dispatchEvent(new Event('change', { bubbles: true }));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示规则配置弹窗
|
||
*/
|
||
showRulesConfigModal() {
|
||
try {
|
||
logger.debug('打开规则配置弹窗');
|
||
|
||
createSettingsDialog({
|
||
title: `<i class="pi pi-list" style="margin-right: 8px;"></i>${tUI('规则管理器')}`,
|
||
dialogClassName: 'rules-config-dialog',
|
||
disableBackdropAndCloseOnClickOutside: true,
|
||
renderContent: (container) => {
|
||
this._createRulesConfigUI(container);
|
||
},
|
||
onSave: async () => {
|
||
if (this.isDirty) {
|
||
try {
|
||
await this._saveConfigToServer();
|
||
this._backupState(); // 保存成功后更新备份
|
||
app.extensionManager.toast.add({
|
||
severity: "success",
|
||
summary: "配置已保存",
|
||
detail: "所有更改已成功保存到服务器",
|
||
life: 3000
|
||
});
|
||
} catch (error) {
|
||
// 错误提示已在 _saveConfigToServer 中处理
|
||
return false; // 保存失败时阻止关闭对话框
|
||
}
|
||
} else {
|
||
app.extensionManager.toast.add({
|
||
severity: "info",
|
||
summary: "无更改",
|
||
detail: "配置没有未保存的更改",
|
||
life: 3000
|
||
});
|
||
}
|
||
return true; // 关闭对话框
|
||
},
|
||
onCancel: () => {
|
||
// createSettingsDialog 的通用逻辑会处理确认
|
||
// 如果用户确认取消,我们在这里恢复状态
|
||
if (this.isDirty) {
|
||
this._restoreState();
|
||
this._renderPromptLists(); // 重新渲染以显示恢复后的状态
|
||
}
|
||
return true; // 允许关闭
|
||
}
|
||
});
|
||
} catch (error) {
|
||
logger.error(`打开规则配置弹窗失败: ${error.message}`);
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: tUI("打开配置失败"),
|
||
detail: error.message || "打开配置弹窗过程中发生错误",
|
||
life: 3000
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建规则配置UI
|
||
* @param {HTMLElement} container 容器元素
|
||
*/
|
||
_createRulesConfigUI(container) {
|
||
const form = document.createElement('form');
|
||
form.onsubmit = (e) => e.preventDefault();
|
||
form.className = 'rules-config-form';
|
||
|
||
// 添加一个隐藏输入,用于脏检查
|
||
const hiddenInput = document.createElement('input');
|
||
hiddenInput.type = 'hidden';
|
||
hiddenInput.name = 'modified_marker';
|
||
hiddenInput.value = 'initial';
|
||
form.appendChild(hiddenInput);
|
||
|
||
// ---创建标签页容器---
|
||
const tabContainer = document.createElement('div');
|
||
tabContainer.className = 'rules-config-tabs';
|
||
|
||
// ---创建标签页头部包装容器(包含标签按钮和添加按钮)---
|
||
const tabHeaderWrapper = document.createElement('div');
|
||
tabHeaderWrapper.className = 'tab-header-wrapper';
|
||
|
||
// ---创建标签页头部---
|
||
const tabHeader = document.createElement('div');
|
||
tabHeader.className = 'tab-header';
|
||
|
||
// ---创建添加按钮容器---
|
||
const addButtonContainer = document.createElement('div');
|
||
addButtonContainer.className = 'tab-header-actions';
|
||
|
||
// 定义标签页配置(包含是否有添加按钮)
|
||
const tabs = [
|
||
{ id: 'expand', title: tUI('提示词优化规则'), subtitle: tUI('提示词优化润色提示词'), addLabel: tUI('添加提示词优化规则') },
|
||
{ id: 'zhVision', title: tUI('中文反推'), subtitle: tUI('图像反推中文提示词'), addLabel: tUI('添加中文反推规则') },
|
||
{ id: 'enVision', title: tUI('英文反推'), subtitle: tUI('图像反推英文提示词'), addLabel: tUI('添加英文反推规则') },
|
||
{ id: 'video', title: tUI('视频反推'), subtitle: tUI('将视频反推提示词'), addLabel: tUI('添加视频反推规则') },
|
||
{ id: 'translate', title: tUI('翻译规则'), subtitle: tUI('大模型翻译规则'), addLabel: null }
|
||
];
|
||
|
||
// 创建标签按钮
|
||
tabs.forEach((tab, index) => {
|
||
const button = this._createRuleTabButton(tab.id, tab.title, tab.subtitle);
|
||
if (index === 0) {
|
||
button.classList.add('active');
|
||
}
|
||
tabHeader.appendChild(button);
|
||
});
|
||
|
||
// 创建各标签页对应的添加按钮(初始只显示第一个)
|
||
tabs.forEach((tab, index) => {
|
||
if (tab.addLabel) {
|
||
const addButton = document.createElement('button');
|
||
addButton.className = 'p-button p-component p-button-sm tab-add-button';
|
||
addButton.type = 'button';
|
||
addButton.dataset.tab = tab.id;
|
||
addButton.innerHTML = `<span class="p-button-icon-left pi pi-plus"></span><span class="p-button-label">${tab.addLabel}</span>`;
|
||
addButton.onclick = () => {
|
||
this._showPromptEditDialog(tab.id, null);
|
||
};
|
||
// 只显示第一个标签页的按钮
|
||
addButton.style.display = index === 0 ? 'inline-flex' : 'none';
|
||
addButtonContainer.appendChild(addButton);
|
||
}
|
||
});
|
||
|
||
tabHeaderWrapper.appendChild(tabHeader);
|
||
tabHeaderWrapper.appendChild(addButtonContainer);
|
||
tabContainer.appendChild(tabHeaderWrapper);
|
||
|
||
// ---创建标签页内容容器---
|
||
const tabContent = document.createElement('div');
|
||
tabContent.className = 'tab-content';
|
||
|
||
// 创建各个标签页内容
|
||
const expandPane = this._createExpandTabPane();
|
||
const zhVisionPane = this._createVisionTabPane('zhVision', '中文反推规则');
|
||
const enVisionPane = this._createVisionTabPane('enVision', '英文反推规则');
|
||
const videoPane = this._createVideoTabPane();
|
||
const translatePane = this._createTranslateTabPane();
|
||
|
||
tabContent.appendChild(expandPane);
|
||
tabContent.appendChild(zhVisionPane);
|
||
tabContent.appendChild(enVisionPane);
|
||
tabContent.appendChild(videoPane);
|
||
tabContent.appendChild(translatePane);
|
||
|
||
tabContainer.appendChild(tabContent);
|
||
form.appendChild(tabContainer);
|
||
container.appendChild(form);
|
||
|
||
// 默认显示第一个标签页
|
||
this._switchRuleTab('expand', tabHeader, tabContent);
|
||
|
||
// 加载系统规则配置
|
||
this._loadSystemPrompts();
|
||
}
|
||
|
||
/**
|
||
* 创建规则标签按钮
|
||
* @param {string} tabId 标签ID
|
||
* @param {string} title 标签标题
|
||
* @param {string} subtitle 标签副标题
|
||
* @returns {HTMLElement} 标签按钮元素
|
||
*/
|
||
_createRuleTabButton(tabId, title, subtitle) {
|
||
const button = document.createElement('button');
|
||
button.className = 'tab-button';
|
||
button.dataset.tab = tabId;
|
||
button.type = 'button';
|
||
|
||
const titleEl = document.createElement('div');
|
||
titleEl.className = 'tab-title';
|
||
titleEl.textContent = title;
|
||
button.appendChild(titleEl);
|
||
|
||
if (subtitle) {
|
||
const subtitleEl = document.createElement('div');
|
||
subtitleEl.className = 'tab-subtitle';
|
||
subtitleEl.textContent = subtitle;
|
||
button.appendChild(subtitleEl);
|
||
}
|
||
|
||
// 点击切换标签
|
||
button.addEventListener('click', () => {
|
||
const tabHeader = button.parentElement;
|
||
// tabHeader 在 tab-header-wrapper 内,tabContent 是 wrapper 的下一个兄弟元素
|
||
const tabHeaderWrapper = tabHeader.parentElement;
|
||
const tabContent = tabHeaderWrapper.nextElementSibling;
|
||
this._switchRuleTab(tabId, tabHeader, tabContent);
|
||
});
|
||
|
||
return button;
|
||
}
|
||
|
||
/**
|
||
* 切换规则标签页
|
||
* @param {string} tabId 标签ID
|
||
* @param {HTMLElement} header 标签头部容器
|
||
* @param {HTMLElement} contentContainer 内容容器
|
||
*/
|
||
_switchRuleTab(tabId, header, contentContainer) {
|
||
// 更新标签按钮状态
|
||
header.querySelectorAll('.tab-button').forEach(btn => {
|
||
if (btn.dataset.tab === tabId) {
|
||
btn.classList.add('active');
|
||
} else {
|
||
btn.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// 显示对应内容
|
||
contentContainer.querySelectorAll('.tab-pane').forEach(pane => {
|
||
pane.style.display = pane.dataset.tab === tabId ? 'block' : 'none';
|
||
});
|
||
|
||
// 切换添加按钮的显示状态
|
||
const actionsContainer = header.parentElement?.querySelector('.tab-header-actions');
|
||
if (actionsContainer) {
|
||
actionsContainer.querySelectorAll('.tab-add-button').forEach(btn => {
|
||
btn.style.display = btn.dataset.tab === tabId ? 'inline-flex' : 'none';
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建扩写规则标签页内容
|
||
* @returns {HTMLElement} 标签页内容元素
|
||
*/
|
||
_createExpandTabPane() {
|
||
const pane = document.createElement('div');
|
||
pane.className = 'tab-pane';
|
||
pane.dataset.tab = 'expand';
|
||
pane.style.display = 'none';
|
||
pane.style.padding = '16px';
|
||
|
||
// 创建规则列表容器
|
||
const listContainer = document.createElement('div');
|
||
listContainer.className = 'prompt-list-container';
|
||
|
||
const listHeader = document.createElement('div');
|
||
listHeader.className = 'prompt-list-header';
|
||
listHeader.innerHTML = `
|
||
<div class="prompt-list-cell status-cell">状态</div>
|
||
<div class="prompt-list-cell name-cell">规则名称</div>
|
||
<div class="prompt-list-cell content-cell">规则内容</div>
|
||
<div class="prompt-list-cell action-cell">操作</div>
|
||
`;
|
||
this._addHeaderResizers(listHeader);
|
||
|
||
const scrollList = document.createElement('div');
|
||
scrollList.className = 'prompt-scroll-list';
|
||
|
||
listContainer.appendChild(listHeader);
|
||
listContainer.appendChild(scrollList);
|
||
pane.appendChild(listContainer);
|
||
|
||
// 存储列表引用
|
||
this.expandScrollList = scrollList;
|
||
|
||
return pane;
|
||
}
|
||
|
||
/**
|
||
* 创建翻译规则标签页内容(只读编辑模式)
|
||
* @returns {HTMLElement} 标签页内容元素
|
||
*/
|
||
_createTranslateTabPane() {
|
||
const pane = document.createElement('div');
|
||
pane.className = 'tab-pane';
|
||
pane.dataset.tab = 'translate';
|
||
pane.style.display = 'none';
|
||
pane.style.padding = '16px';
|
||
|
||
|
||
|
||
// 创建翻译规则列表容器
|
||
const listContainer = document.createElement('div');
|
||
listContainer.className = 'prompt-list-container';
|
||
|
||
const listHeader = document.createElement('div');
|
||
listHeader.className = 'prompt-list-header';
|
||
listHeader.innerHTML = `
|
||
<div class="prompt-list-cell name-cell">规则名称</div>
|
||
<div class="prompt-list-cell content-cell">规则内容</div>
|
||
<div class="prompt-list-cell action-cell">操作</div>
|
||
`;
|
||
this._addHeaderResizers(listHeader);
|
||
|
||
const scrollList = document.createElement('div');
|
||
scrollList.className = 'prompt-scroll-list';
|
||
|
||
listContainer.appendChild(listHeader);
|
||
listContainer.appendChild(scrollList);
|
||
pane.appendChild(listContainer);
|
||
// 创建提示信息
|
||
const notice = document.createElement('div');
|
||
notice.className = 'rule-pane-notice';
|
||
notice.innerHTML = `<i class="pi pi-info-circle"></i> ${tUI('翻译规则仅支持编辑,不支持新增和删除')}`;
|
||
pane.appendChild(notice);
|
||
|
||
// 存储列表引用
|
||
this.translateScrollList = scrollList;
|
||
|
||
return pane;
|
||
}
|
||
|
||
/**
|
||
* 创建反推规则标签页内容
|
||
* @param {string} type 类型 (zhVision/enVision)
|
||
* @param {string} title 标题
|
||
* @returns {HTMLElement} 标签页内容元素
|
||
*/
|
||
_createVisionTabPane(type, title) {
|
||
const pane = document.createElement('div');
|
||
pane.className = 'tab-pane';
|
||
pane.dataset.tab = type;
|
||
pane.style.display = 'none';
|
||
pane.style.padding = '16px';
|
||
|
||
// 创建规则列表容器
|
||
const listContainer = document.createElement('div');
|
||
listContainer.className = 'prompt-list-container vision-prompt-list-container';
|
||
|
||
const listHeader = document.createElement('div');
|
||
listHeader.className = 'prompt-list-header';
|
||
listHeader.innerHTML = `
|
||
<div class="prompt-list-cell status-cell">状态</div>
|
||
<div class="prompt-list-cell name-cell">规则名称</div>
|
||
<div class="prompt-list-cell content-cell">规则内容</div>
|
||
<div class="prompt-list-cell action-cell">操作</div>
|
||
`;
|
||
this._addHeaderResizers(listHeader);
|
||
|
||
const scrollList = document.createElement('div');
|
||
scrollList.className = 'prompt-scroll-list';
|
||
|
||
listContainer.appendChild(listHeader);
|
||
listContainer.appendChild(scrollList);
|
||
pane.appendChild(listContainer);
|
||
|
||
// 存储列表引用
|
||
if (type === 'zhVision') {
|
||
this.zhVisionScrollList = scrollList;
|
||
} else {
|
||
this.enVisionScrollList = scrollList;
|
||
}
|
||
|
||
return pane;
|
||
}
|
||
|
||
/**
|
||
* 创建视频反推规则标签页内容
|
||
* @returns {HTMLElement} 标签页内容元素
|
||
*/
|
||
_createVideoTabPane() {
|
||
const pane = document.createElement('div');
|
||
pane.className = 'tab-pane';
|
||
pane.dataset.tab = 'video';
|
||
pane.style.display = 'none';
|
||
pane.style.padding = '16px';
|
||
|
||
// 创建规则列表容器
|
||
const listContainer = document.createElement('div');
|
||
listContainer.className = 'prompt-list-container video-prompt-list-container';
|
||
|
||
const listHeader = document.createElement('div');
|
||
listHeader.className = 'prompt-list-header';
|
||
listHeader.innerHTML = `
|
||
<div class="prompt-list-cell status-cell">状态</div>
|
||
<div class="prompt-list-cell name-cell">规则名称</div>
|
||
<div class="prompt-list-cell content-cell">规则内容</div>
|
||
<div class="prompt-list-cell action-cell">操作</div>
|
||
`;
|
||
this._addHeaderResizers(listHeader);
|
||
|
||
const scrollList = document.createElement('div');
|
||
scrollList.className = 'prompt-scroll-list';
|
||
|
||
listContainer.appendChild(listHeader);
|
||
listContainer.appendChild(scrollList);
|
||
pane.appendChild(listContainer);
|
||
|
||
// 存储列表引用
|
||
this.videoScrollList = scrollList;
|
||
|
||
return pane;
|
||
}
|
||
|
||
/**
|
||
* 加载系统规则配置
|
||
*/
|
||
async _loadSystemPrompts() {
|
||
try {
|
||
const response = await fetch(APIService.getApiUrl('/config/system_prompts'));
|
||
if (!response.ok) {
|
||
throw new Error(`加载系统规则配置失败: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
this.systemPrompts = data;
|
||
|
||
// 获取激活的规则ID
|
||
const activePrompts = data.active_prompts || {};
|
||
const activeExpandId = activePrompts.expand;
|
||
const activeZhVisionId = activePrompts.vision_zh;
|
||
const activeEnVisionId = activePrompts.vision_en;
|
||
|
||
// 输出加载的激活ID
|
||
logger.debug(`加载的激活规则ID | expand:${activeExpandId || '无'} | vision_zh:${activeZhVisionId || '无'} | vision_en:${activeEnVisionId || '无'}`);
|
||
|
||
// 转换扩写规则数据
|
||
this.expandPrompts = [];
|
||
if (data.expand_prompts) {
|
||
// 保存原始顺序,以便在保存时恢复
|
||
this.originalExpandOrder = Object.keys(data.expand_prompts);
|
||
|
||
// 使用原始顺序遍历键
|
||
this.originalExpandOrder.forEach(key => {
|
||
const prompt = data.expand_prompts[key];
|
||
// 使用配置中的键作为ID,而不是生成新的ID
|
||
this.expandPrompts.push({
|
||
id: key,
|
||
name: prompt.name || key, // 如果没有名称,使用键作为名称
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
content: prompt.content,
|
||
isActive: key === activeExpandId, // 根据active_prompts判断是否激活
|
||
order: this.originalExpandOrder.indexOf(key) // 保存原始顺序
|
||
});
|
||
});
|
||
|
||
// 如果没有找到激活的规则,则激活第一个
|
||
if (!activeExpandId && this.expandPrompts.length > 0) {
|
||
this.expandPrompts[0].isActive = true;
|
||
this.activeExpandPromptId = this.expandPrompts[0].id;
|
||
} else {
|
||
this.activeExpandPromptId = activeExpandId;
|
||
}
|
||
|
||
// 按原始顺序排序
|
||
this.expandPrompts.sort((a, b) => a.order - b.order);
|
||
}
|
||
|
||
// 转换反推规则数据
|
||
this.zhVisionPrompts = [];
|
||
this.enVisionPrompts = [];
|
||
if (data.vision_prompts) {
|
||
// 保存原始顺序
|
||
this.originalVisionOrder = Object.keys(data.vision_prompts);
|
||
|
||
// 分别存储中文和英文规则的键
|
||
const zhKeys = [];
|
||
const enKeys = [];
|
||
|
||
// 先分类所有键
|
||
this.originalVisionOrder.forEach(key => {
|
||
const prompt = data.vision_prompts[key];
|
||
// 根据ID或内容判断是中文还是英文
|
||
const isChinese = key.includes('zh') || /[\u4e00-\u9fa5]/.test(prompt.content);
|
||
|
||
if (isChinese) {
|
||
zhKeys.push(key);
|
||
} else {
|
||
enKeys.push(key);
|
||
}
|
||
});
|
||
|
||
// 处理中文规则
|
||
zhKeys.forEach((key, index) => {
|
||
const prompt = data.vision_prompts[key];
|
||
this.zhVisionPrompts.push({
|
||
id: key,
|
||
name: prompt.name || key,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
content: prompt.content,
|
||
isActive: key === activeZhVisionId,
|
||
order: this.originalVisionOrder.indexOf(key) // 保存原始顺序
|
||
});
|
||
});
|
||
|
||
// 处理英文规则
|
||
enKeys.forEach((key, index) => {
|
||
const prompt = data.vision_prompts[key];
|
||
this.enVisionPrompts.push({
|
||
id: key,
|
||
name: prompt.name || key,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
content: prompt.content,
|
||
isActive: key === activeEnVisionId,
|
||
order: this.originalVisionOrder.indexOf(key) // 保存原始顺序
|
||
});
|
||
});
|
||
|
||
// 按原始顺序排序
|
||
this.zhVisionPrompts.sort((a, b) => a.order - b.order);
|
||
this.enVisionPrompts.sort((a, b) => a.order - b.order);
|
||
|
||
// 如果没有找到激活的中文规则,则激活第一个
|
||
if (!activeZhVisionId && this.zhVisionPrompts.length > 0) {
|
||
this.zhVisionPrompts[0].isActive = true;
|
||
this.activeZhVisionPromptId = this.zhVisionPrompts[0].id;
|
||
} else {
|
||
this.activeZhVisionPromptId = activeZhVisionId;
|
||
}
|
||
|
||
// 如果没有找到激活的英文规则,则激活第一个
|
||
if (!activeEnVisionId && this.enVisionPrompts.length > 0) {
|
||
this.enVisionPrompts[0].isActive = true;
|
||
this.activeEnVisionPromptId = this.enVisionPrompts[0].id;
|
||
} else {
|
||
this.activeEnVisionPromptId = activeEnVisionId;
|
||
}
|
||
}
|
||
|
||
// 转换翻译规则数据(只读编辑模式)
|
||
this.translatePrompts = [];
|
||
if (data.translate_prompts) {
|
||
Object.keys(data.translate_prompts).forEach(key => {
|
||
const prompt = data.translate_prompts[key];
|
||
this.translatePrompts.push({
|
||
id: key,
|
||
name: key, // 使用键作为名称
|
||
content: prompt.content,
|
||
role: prompt.role || 'system',
|
||
isReadOnly: true // 标记为只读(不可增删)
|
||
});
|
||
});
|
||
}
|
||
|
||
// 转换视频反推规则数据
|
||
this.videoPrompts = [];
|
||
const activeVideoId = activePrompts.video;
|
||
if (data.video_prompts) {
|
||
const videoKeys = Object.keys(data.video_prompts);
|
||
videoKeys.forEach((key, index) => {
|
||
const prompt = data.video_prompts[key];
|
||
this.videoPrompts.push({
|
||
id: key,
|
||
name: prompt.name || key,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
content: prompt.content,
|
||
isActive: key === activeVideoId,
|
||
order: index
|
||
});
|
||
});
|
||
|
||
// 如果没有找到激活的视频反推规则,则激活第一个
|
||
if (!activeVideoId && this.videoPrompts.length > 0) {
|
||
this.videoPrompts[0].isActive = true;
|
||
this.activeVideoPromptId = this.videoPrompts[0].id;
|
||
} else {
|
||
this.activeVideoPromptId = activeVideoId;
|
||
}
|
||
}
|
||
|
||
// 备份初始状态
|
||
this._backupState();
|
||
|
||
// 渲染列表
|
||
this._renderPromptLists();
|
||
|
||
} catch (error) {
|
||
logger.error("加载系统规则配置失败:", error);
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: "加载失败",
|
||
detail: error.message || "加载系统规则配置过程中发生错误",
|
||
life: 3000
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染规则列表
|
||
*/
|
||
_renderPromptLists() {
|
||
logger.debug(`开始渲染规则列表 | 提示词优化规则:${this.expandPrompts.length}个 | 翻译规则:${this.translatePrompts?.length || 0}个 | 中文反推:${this.zhVisionPrompts.length}个 | 英文反推:${this.enVisionPrompts.length}个`);
|
||
|
||
// 渲染扩写规则列表
|
||
if (this.expandScrollList) {
|
||
this._renderPromptList(this.expandScrollList, this.expandPrompts, 'expand');
|
||
logger.debug(`渲染提示词优化规则列表完成`);
|
||
} else {
|
||
logger.error(`提示词优化规则列表容器不存在`);
|
||
}
|
||
|
||
// 渲染翻译规则列表(只读编辑模式)
|
||
if (this.translateScrollList) {
|
||
this._renderTranslateList(this.translateScrollList, this.translatePrompts || []);
|
||
logger.debug(`渲染翻译规则列表完成`);
|
||
}
|
||
|
||
// 渲染中文反推规则列表
|
||
if (this.zhVisionScrollList) {
|
||
this._renderPromptList(this.zhVisionScrollList, this.zhVisionPrompts, 'zhVision');
|
||
logger.debug(`渲染中文反推规则列表完成`);
|
||
} else {
|
||
logger.error(`中文反推规则列表容器不存在`);
|
||
}
|
||
|
||
// 渲染英文反推规则列表
|
||
if (this.enVisionScrollList) {
|
||
this._renderPromptList(this.enVisionScrollList, this.enVisionPrompts, 'enVision');
|
||
logger.debug(`渲染英文反推规则列表完成`);
|
||
} else {
|
||
logger.error(`英文反推规则列表容器不存在`);
|
||
}
|
||
|
||
// 渲染视频反推规则列表
|
||
if (this.videoScrollList) {
|
||
this._renderPromptList(this.videoScrollList, this.videoPrompts || [], 'video');
|
||
logger.debug(`渲染视频反推规则列表完成`);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 渲染翻译规则列表(只读编辑模式)
|
||
* @param {HTMLElement} container 列表容器
|
||
* @param {Array} prompts 翻译规则数组
|
||
*/
|
||
_renderTranslateList(container, prompts) {
|
||
container.innerHTML = '';
|
||
|
||
prompts.forEach(prompt => {
|
||
const row = document.createElement('div');
|
||
row.className = 'prompt-list-row translate-row';
|
||
row.dataset.promptId = prompt.id;
|
||
row.dataset.type = 'translate';
|
||
|
||
// 名称列
|
||
const nameCell = document.createElement('div');
|
||
nameCell.className = 'prompt-list-cell name-cell';
|
||
nameCell.textContent = prompt.name;
|
||
nameCell.title = prompt.name;
|
||
|
||
// 内容列
|
||
const contentCell = document.createElement('div');
|
||
contentCell.className = 'prompt-list-cell content-cell';
|
||
contentCell.textContent = prompt.content;
|
||
contentCell.title = prompt.content;
|
||
|
||
// 操作列(只有编辑按钮)
|
||
const actionCell = document.createElement('div');
|
||
actionCell.className = 'prompt-list-cell action-cell';
|
||
|
||
const editBtn = document.createElement('button');
|
||
editBtn.className = 'prompt-action-btn edit-btn';
|
||
editBtn.innerHTML = '<span class="pi pi-pencil"></span>';
|
||
editBtn.title = tUI('编辑翻译规则');
|
||
editBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this._showTranslateEditDialog(prompt.id);
|
||
};
|
||
|
||
actionCell.appendChild(editBtn);
|
||
|
||
row.appendChild(nameCell);
|
||
row.appendChild(contentCell);
|
||
row.appendChild(actionCell);
|
||
|
||
container.appendChild(row);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 显示翻译规则编辑对话框
|
||
* @param {string} promptId 翻译规则ID
|
||
*/
|
||
_showTranslateEditDialog(promptId) {
|
||
const prompt = (this.translatePrompts || []).find(p => p.id === promptId);
|
||
if (!prompt) {
|
||
logger.error(`找不到翻译规则: ${promptId}`);
|
||
return;
|
||
}
|
||
|
||
createSettingsDialog({
|
||
title: `<i class="pi pi-pencil" style="margin-right: 8px;"></i>${tUI('编辑翻译规则')}`,
|
||
isConfirmDialog: true,
|
||
dialogClassName: 'translate-edit-dialog',
|
||
renderContent: (content) => {
|
||
content.className += ' dialog-form-content';
|
||
content.style.padding = '1rem';
|
||
content.style.display = 'flex';
|
||
content.style.flexDirection = 'column';
|
||
content.style.flex = '1';
|
||
|
||
// 显示规则名称(只读)
|
||
const nameGroup = document.createElement('div');
|
||
nameGroup.className = 'settings-form-group-item';
|
||
nameGroup.innerHTML = `
|
||
<label class="settings-form-label">规则名称</label>
|
||
<div class="settings-input-wrapper">
|
||
<input type="text" class="settings-input" value="${prompt.name}" disabled style="opacity: 0.7;" />
|
||
</div>
|
||
`;
|
||
nameGroup.style.marginBottom = '10px';
|
||
|
||
// 创建内容编辑区
|
||
const contentTextarea = createTextareaGroup('规则内容', tUI('请输入翻译规则内容'), 10);
|
||
contentTextarea.group.style.flex = '1';
|
||
contentTextarea.group.style.display = 'flex';
|
||
contentTextarea.group.style.flexDirection = 'column';
|
||
contentTextarea.textarea.value = prompt.content || '';
|
||
|
||
content.appendChild(nameGroup);
|
||
content.appendChild(contentTextarea.group);
|
||
|
||
content.contentTextarea = contentTextarea.textarea;
|
||
},
|
||
onSave: (content) => {
|
||
const newContent = content.contentTextarea.value.trim();
|
||
|
||
if (!newContent) {
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: tUI("编辑失败"),
|
||
detail: "规则内容不能为空",
|
||
life: 3000
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// 更新翻译规则内容
|
||
prompt.content = newContent;
|
||
|
||
// 标记为已修改
|
||
this._setDirty();
|
||
|
||
// 重新渲染列表
|
||
this._renderPromptLists();
|
||
|
||
return true;
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 渲染单个规则列表
|
||
* @param {HTMLElement} container 列表容器
|
||
* @param {Array} prompts 规则数组
|
||
* @param {string} type 类型
|
||
*/
|
||
_renderPromptList(container, prompts, type) {
|
||
container.innerHTML = '';
|
||
|
||
prompts.forEach(prompt => {
|
||
const row = document.createElement('div');
|
||
row.className = 'prompt-list-row';
|
||
row.dataset.promptId = prompt.id;
|
||
row.dataset.type = type;
|
||
row.dataset.order = prompt.order; // 添加顺序属性到DOM元素,方便调试
|
||
|
||
// 状态列 - 可点击切换状态
|
||
const statusCell = document.createElement('div');
|
||
statusCell.className = 'prompt-list-cell status-cell';
|
||
const statusIcon = document.createElement('span');
|
||
statusIcon.className = prompt.isActive ? 'pi pi-check-circle active-status' : 'pi pi-circle-off inactive-status';
|
||
statusIcon.title = prompt.isActive ? tUI('当前使用中,点击取消激活') : tUI('未激活,点击激活');
|
||
statusCell.appendChild(statusIcon);
|
||
statusCell.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this._togglePromptActive(type, prompt.id);
|
||
};
|
||
|
||
// 名称列
|
||
const nameCell = document.createElement('div');
|
||
nameCell.className = 'prompt-list-cell name-cell';
|
||
// 使用name属性显示名称,而不是id
|
||
nameCell.textContent = prompt.name;
|
||
nameCell.title = prompt.name;
|
||
|
||
// 添加列宽调整手柄
|
||
const nameResizer = document.createElement('div');
|
||
nameResizer.className = 'column-resizer';
|
||
nameCell.appendChild(nameResizer);
|
||
this._addColumnResizer(nameResizer, nameCell);
|
||
|
||
// 内容列
|
||
const contentCell = document.createElement('div');
|
||
contentCell.className = 'prompt-list-cell content-cell';
|
||
const contentPreview = prompt.content.length > 50 ?
|
||
prompt.content.substring(0, 50) + '...' : prompt.content;
|
||
contentCell.textContent = contentPreview;
|
||
contentCell.title = prompt.content;
|
||
|
||
// 添加列宽调整手柄
|
||
const contentResizer = document.createElement('div');
|
||
contentResizer.className = 'column-resizer';
|
||
contentCell.appendChild(contentResizer);
|
||
this._addColumnResizer(contentResizer, contentCell);
|
||
|
||
// 操作列
|
||
const actionCell = document.createElement('div');
|
||
actionCell.className = 'prompt-list-cell action-cell';
|
||
|
||
// 编辑按钮
|
||
const editButton = document.createElement('button');
|
||
editButton.className = 'prompt-action-btn edit-btn';
|
||
editButton.innerHTML = '<span class="pi pi-pencil"></span>';
|
||
editButton.title = tUI('编辑');
|
||
editButton.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this._showPromptEditDialog(type, prompt.id);
|
||
};
|
||
|
||
// 删除按钮
|
||
const deleteButton = document.createElement('button');
|
||
deleteButton.className = 'prompt-action-btn delete-btn';
|
||
deleteButton.innerHTML = '<span class="pi pi-trash"></span>';
|
||
deleteButton.title = tUI('删除');
|
||
deleteButton.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this._deletePrompt(type, prompt.id);
|
||
};
|
||
|
||
actionCell.appendChild(editButton);
|
||
actionCell.appendChild(deleteButton);
|
||
|
||
row.appendChild(statusCell);
|
||
row.appendChild(nameCell);
|
||
row.appendChild(contentCell);
|
||
row.appendChild(actionCell);
|
||
|
||
container.appendChild(row);
|
||
});
|
||
|
||
// 初始化拖拽排序
|
||
this._initSortable(container, type);
|
||
}
|
||
|
||
/**
|
||
* 获取已存在的分类列表
|
||
* 从所有规则类型中提取已使用的分类
|
||
* @returns {Array<{value: string, text: string}>} 分类选项列表
|
||
*/
|
||
_getExistingCategories() {
|
||
const categories = new Set();
|
||
|
||
// 从所有规则类型中提取分类
|
||
const allPrompts = [
|
||
...this.expandPrompts,
|
||
...this.zhVisionPrompts,
|
||
...this.enVisionPrompts,
|
||
...this.videoPrompts
|
||
];
|
||
|
||
allPrompts.forEach(prompt => {
|
||
if (prompt.category && typeof prompt.category === 'string') {
|
||
categories.add(prompt.category);
|
||
}
|
||
});
|
||
|
||
// 转换为下拉选项格式
|
||
return Array.from(categories)
|
||
.sort() // 按字母顺序排序
|
||
.map(cat => ({ value: cat, text: cat }));
|
||
}
|
||
|
||
/**
|
||
* 显示规则编辑对话框
|
||
* @param {string} type 类型
|
||
* @param {string|null} promptId 规则ID,null表示新建
|
||
*/
|
||
_showPromptEditDialog(type, promptId) {
|
||
this._showPromptConfigDialog(type, promptId);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 显示通用的规则配置弹窗
|
||
* @param {string} defaultType 默认类型
|
||
* @param {string|null} promptId 规则ID,null表示新建
|
||
*/
|
||
_showPromptConfigDialog(defaultType = 'expand', promptId = null) {
|
||
const isEdit = !!promptId;
|
||
let editData = null;
|
||
|
||
// 如果是编辑模式,查找对应的数据
|
||
if (isEdit) {
|
||
if (defaultType === 'expand') {
|
||
editData = this.expandPrompts.find(p => p.id === promptId);
|
||
} else if (defaultType === 'zhVision') {
|
||
editData = this.zhVisionPrompts.find(p => p.id === promptId);
|
||
} else if (defaultType === 'enVision') {
|
||
editData = this.enVisionPrompts.find(p => p.id === promptId);
|
||
} else if (defaultType === 'video') {
|
||
editData = this.videoPrompts.find(p => p.id === promptId);
|
||
}
|
||
}
|
||
|
||
createSettingsDialog({
|
||
title: isEdit ? tUI('编辑规则') : tUI('添加规则'),
|
||
isConfirmDialog: true,
|
||
dialogClassName: 'prompt-edit-dialog',
|
||
disableBackdropAndCloseOnClickOutside: true,
|
||
saveButtonText: isEdit ? tUI('保存') : tUI('添加'),
|
||
cancelButtonText: tUI('取消'),
|
||
renderContent: (content) => {
|
||
content.className += ' dialog-form-content';
|
||
content.style.padding = '10px 40px'; // 增加两侧留白
|
||
content.style.display = 'flex';
|
||
content.style.flexDirection = 'column';
|
||
content.style.flex = '1';
|
||
content.style.overflow = 'hidden'; // 整体不滚动,内部 textarea 滚动
|
||
|
||
// 创建规则名称输入框
|
||
const nameInput = createInputGroup('规则名称', tUI('请输入规则名称'));
|
||
nameInput.group.style.marginBottom = '10px';
|
||
if (isEdit && editData) {
|
||
nameInput.input.value = editData.name || '';
|
||
}
|
||
|
||
// 创建规则类型下拉框
|
||
const typeOptions = [
|
||
{ value: 'expand', text: '提示词优化' },
|
||
{ value: 'zhVision', text: '反推(中文)' },
|
||
{ value: 'enVision', text: '反推(英文)' },
|
||
{ value: 'video', text: '视频反推' }
|
||
];
|
||
const typeSelect = createSelectGroup('规则类型', typeOptions, defaultType);
|
||
typeSelect.group.style.marginBottom = '10px';
|
||
|
||
// 创建分类选择框(可输入)
|
||
const categoryOptions = this._getExistingCategories();
|
||
const initialCategory = (isEdit && editData) ? (editData.category || '') : '';
|
||
const categoryComboBox = createComboBoxGroup('分类', categoryOptions, initialCategory, {
|
||
placeholder: '输入或选择分类(可留空)',
|
||
emptyText: '暂无分类'
|
||
});
|
||
categoryComboBox.group.style.flex = '1';
|
||
categoryComboBox.group.style.marginBottom = '0';
|
||
|
||
// 创建显示位置选择组件
|
||
const showInOptions = [
|
||
{ value: 'frontend', label: '小助手上显示' },
|
||
{ value: 'node', label: '节点上显示' }
|
||
];
|
||
// 获取初始值:编辑时从规则数据读取,新建时默认都选中
|
||
const initialShowIn = (isEdit && editData && editData.showIn)
|
||
? editData.showIn
|
||
: ['frontend', 'node'];
|
||
const showInSelectButton = createSelectButtonGroup('', showInOptions, initialShowIn, {
|
||
mode: 'multiple',
|
||
size: 'small',
|
||
allowEmpty: true
|
||
});
|
||
showInSelectButton.group.style.marginBottom = '0';
|
||
|
||
// 创建行容器放置分类和显示位置组件
|
||
const categoryRow = document.createElement('div');
|
||
categoryRow.style.display = 'flex';
|
||
categoryRow.style.flexDirection = 'row';
|
||
categoryRow.style.alignItems = 'flex-end';
|
||
categoryRow.style.gap = '1rem';
|
||
categoryRow.style.marginBottom = '10px';
|
||
categoryRow.appendChild(categoryComboBox.group);
|
||
categoryRow.appendChild(showInSelectButton.group);
|
||
|
||
// 创建规则内容多行输入框
|
||
const contentTextarea = createTextareaGroup('规则内容', tUI('请输入规则内容'));
|
||
contentTextarea.group.style.marginBottom = '0';
|
||
contentTextarea.group.style.flex = '1';
|
||
contentTextarea.group.style.display = 'flex';
|
||
contentTextarea.group.style.flexDirection = 'column';
|
||
|
||
// 确保 textarea 填充容器
|
||
const textareaContainer = contentTextarea.group.querySelector('.float-label-container');
|
||
if (textareaContainer) {
|
||
textareaContainer.style.flex = '1';
|
||
textareaContainer.style.display = 'flex';
|
||
textareaContainer.style.flexDirection = 'column';
|
||
contentTextarea.textarea.style.flex = '1';
|
||
}
|
||
|
||
if (isEdit && editData) {
|
||
contentTextarea.textarea.value = editData.content || '';
|
||
}
|
||
|
||
content.appendChild(nameInput.group);
|
||
content.appendChild(typeSelect.group);
|
||
content.appendChild(categoryRow);
|
||
content.appendChild(contentTextarea.group);
|
||
|
||
content.nameInput = nameInput.input;
|
||
content.typeSelect = typeSelect.select;
|
||
content.categoryInput = categoryComboBox.input;
|
||
content.showInSelectButton = showInSelectButton;
|
||
content.contentTextarea = contentTextarea.textarea;
|
||
|
||
// 聚焦名称输入框
|
||
setTimeout(() => {
|
||
nameInput.input.focus();
|
||
}, 100);
|
||
},
|
||
onSave: (content) => {
|
||
const name = content.nameInput.value.trim();
|
||
const type = content.typeSelect.value;
|
||
const category = content.categoryInput.value.trim();
|
||
const showIn = content.showInSelectButton.getValue();
|
||
const promptContent = content.contentTextarea.value.trim();
|
||
|
||
if (!name || !promptContent) {
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: isEdit ? tUI("编辑失败") : tUI("添加失败"),
|
||
detail: "规则名称和内容不能为空",
|
||
life: 3000
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 根据类型获取对应的数组
|
||
let targetArray;
|
||
if (type === 'expand') {
|
||
targetArray = this.expandPrompts;
|
||
} else if (type === 'zhVision') {
|
||
targetArray = this.zhVisionPrompts;
|
||
} else if (type === 'enVision') {
|
||
targetArray = this.enVisionPrompts;
|
||
} else if (type === 'video') {
|
||
targetArray = this.videoPrompts;
|
||
} else {
|
||
logger.error(`未知的规则类型: ${type}`);
|
||
return;
|
||
}
|
||
|
||
// 检查名称是否重复(排除编辑时的自身)
|
||
const isDuplicate = targetArray.some(p =>
|
||
(p.name === name || p.id === name) && (!isEdit || p.id !== promptId)
|
||
);
|
||
|
||
if (isDuplicate) {
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: isEdit ? tUI("编辑失败") : tUI("添加失败"),
|
||
detail: `${tUI('规则名称')} "${name}" ${tUI('已存在')}`,
|
||
life: 3000
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isEdit) {
|
||
// 编辑模式:更新现有数据
|
||
if (editData) {
|
||
// 如果类型发生变化,需要从原数组中移除,添加到新数组
|
||
if (type !== defaultType) {
|
||
// 从原数组中移除
|
||
let originalArray;
|
||
if (defaultType === 'expand') {
|
||
originalArray = this.expandPrompts;
|
||
} else if (defaultType === 'zhVision') {
|
||
originalArray = this.zhVisionPrompts;
|
||
} else if (defaultType === 'enVision') {
|
||
originalArray = this.enVisionPrompts;
|
||
}
|
||
const index = originalArray.findIndex(p => p.id === promptId);
|
||
if (index !== -1) {
|
||
originalArray.splice(index, 1);
|
||
}
|
||
|
||
// 添加到新数组
|
||
// 为不同类型的规则设置不同的ID前缀
|
||
let newId;
|
||
if (type === 'expand') {
|
||
newId = 'expand_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'zhVision') {
|
||
newId = 'vision_zh_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'enVision') {
|
||
newId = 'vision_en_' + name.replace(/\s+/g, '_');
|
||
} else {
|
||
newId = this._generateId();
|
||
}
|
||
|
||
const newPrompt = {
|
||
id: newId,
|
||
name: name,
|
||
category: category,
|
||
showIn: showIn,
|
||
content: promptContent,
|
||
isActive: false,
|
||
order: targetArray.length // 设置顺序为当前数组长度
|
||
};
|
||
targetArray.push(newPrompt);
|
||
logger.debug(`跨类型编辑规则 | 原类型:${defaultType} | 新类型:${type} | 新ID:${newId}`);
|
||
} else {
|
||
// 同类型编辑,直接更新
|
||
const oldName = editData.name;
|
||
editData.name = name;
|
||
editData.category = category;
|
||
editData.showIn = showIn;
|
||
editData.content = promptContent;
|
||
|
||
// 如果名称发生了变化,则更新ID
|
||
if (oldName !== name) {
|
||
const oldId = editData.id;
|
||
let newId;
|
||
|
||
// 根据类型设置不同的ID前缀
|
||
if (type === 'expand') {
|
||
newId = 'expand_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'zhVision') {
|
||
newId = 'vision_zh_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'enVision') {
|
||
newId = 'vision_en_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'video') {
|
||
newId = 'video_' + name.replace(/\s+/g, '_');
|
||
} else {
|
||
newId = name;
|
||
}
|
||
|
||
editData.id = newId;
|
||
logger.debug(`更新规则ID | 类型:${type} | 旧ID:${oldId} | 新ID:${newId}`);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 添加模式:创建新数据
|
||
// 根据类型设置不同的ID前缀
|
||
let newId;
|
||
if (type === 'expand') {
|
||
newId = 'expand_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'zhVision') {
|
||
newId = 'vision_zh_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'enVision') {
|
||
newId = 'vision_en_' + name.replace(/\s+/g, '_');
|
||
} else if (type === 'video') {
|
||
newId = 'video_' + name.replace(/\s+/g, '_');
|
||
} else {
|
||
newId = name;
|
||
}
|
||
|
||
const newPrompt = {
|
||
id: newId,
|
||
name: name,
|
||
category: category,
|
||
showIn: showIn,
|
||
content: promptContent,
|
||
isActive: false, // 默认不激活
|
||
order: targetArray.length // 设置顺序为当前数组长度,确保添加到末尾
|
||
};
|
||
targetArray.push(newPrompt);
|
||
|
||
// 输出调试信息
|
||
logger.debug(`添加新规则 | 类型:${type} | ID:${newId} | 顺序:${newPrompt.order} | 数组长度:${targetArray.length}`);
|
||
}
|
||
|
||
// 标记为已修改
|
||
this._setDirty();
|
||
|
||
// 重新渲染对应的列表
|
||
this._renderPromptLists();
|
||
|
||
return true;
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 生成唯一ID
|
||
* @returns {string} 唯一ID
|
||
*/
|
||
_generateId() {
|
||
return 'prompt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
}
|
||
|
||
/**
|
||
* 刷新所有规则列表
|
||
* 注意:为了保持一致性,建议直接使用_renderPromptLists方法
|
||
*/
|
||
_refreshPromptLists() {
|
||
// 直接调用_renderPromptLists方法,确保一致性
|
||
this._renderPromptLists();
|
||
}
|
||
|
||
/**
|
||
* 切换规则激活状态
|
||
* @param {string} type 类型
|
||
* @param {string} promptId 规则ID
|
||
*/
|
||
_togglePromptActive(type, promptId) {
|
||
// 获取对应的数据数组
|
||
let dataArray;
|
||
let activeIdProperty;
|
||
let configKey; // 用于保存到配置中的键名
|
||
|
||
switch (type) {
|
||
case 'expand':
|
||
dataArray = this.expandPrompts;
|
||
activeIdProperty = 'activeExpandPromptId';
|
||
configKey = 'expand';
|
||
break;
|
||
case 'zhVision':
|
||
dataArray = this.zhVisionPrompts;
|
||
activeIdProperty = 'activeZhVisionPromptId';
|
||
configKey = 'vision_zh';
|
||
break;
|
||
case 'enVision':
|
||
dataArray = this.enVisionPrompts;
|
||
activeIdProperty = 'activeEnVisionPromptId';
|
||
configKey = 'vision_en';
|
||
break;
|
||
case 'video':
|
||
dataArray = this.videoPrompts;
|
||
activeIdProperty = 'activeVideoPromptId';
|
||
configKey = 'video';
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
|
||
|
||
// 找到目标规则
|
||
const targetPrompt = dataArray.find(p => p.id === promptId);
|
||
if (!targetPrompt) return;
|
||
|
||
// 记录之前的激活状态
|
||
const oldActiveId = this[activeIdProperty];
|
||
const wasActive = targetPrompt.isActive;
|
||
|
||
// 如果当前规则已激活,则取消激活
|
||
if (targetPrompt.isActive) {
|
||
targetPrompt.isActive = false;
|
||
this[activeIdProperty] = null;
|
||
logger.debug(`取消激活规则 | 类型:${type} | ID:${promptId} | 配置键:${configKey}`);
|
||
} else {
|
||
// 取消其他规则的激活状态
|
||
dataArray.forEach(p => p.isActive = false);
|
||
// 激活当前规则
|
||
targetPrompt.isActive = true;
|
||
this[activeIdProperty] = promptId;
|
||
logger.debug(`激活规则 | 类型:${type} | ID:${promptId} | 配置键:${configKey} | 旧ID:${oldActiveId}`);
|
||
}
|
||
|
||
// 标记为已修改
|
||
this._setDirty();
|
||
|
||
// 重新渲染列表
|
||
this._renderPromptLists();
|
||
|
||
// 输出激活状态变化
|
||
logger.debug(`激活状态变化 | 类型:${type} | 规则:${targetPrompt.name} | ID:${promptId} | 旧状态:${wasActive} | 新状态:${targetPrompt.isActive} | 实例属性:${this[activeIdProperty]}`);
|
||
|
||
// 如果系统规则对象存在,直接更新其中的active_prompts
|
||
if (this.systemPrompts && this.systemPrompts.active_prompts) {
|
||
const oldValue = this.systemPrompts.active_prompts[configKey];
|
||
this.systemPrompts.active_prompts[configKey] = targetPrompt.isActive ? promptId : null;
|
||
logger.debug(`更新系统规则对象 | 配置键:${configKey} | 旧值:${oldValue} | 新值:${this.systemPrompts.active_prompts[configKey]}`);
|
||
}
|
||
|
||
logger.debug(`${type} 规则状态切换: ${promptId} -> ${targetPrompt.isActive ? '激活' : '取消激活'}`);
|
||
}
|
||
|
||
/**
|
||
* 删除规则
|
||
* @param {string} type 类型
|
||
* @param {string} promptId 规则ID
|
||
*/
|
||
_deletePrompt(type, promptId) {
|
||
// 获取对应的数据数组
|
||
let dataArray;
|
||
switch (type) {
|
||
case 'expand':
|
||
dataArray = this.expandPrompts;
|
||
break;
|
||
case 'zhVision':
|
||
dataArray = this.zhVisionPrompts;
|
||
break;
|
||
case 'enVision':
|
||
dataArray = this.enVisionPrompts;
|
||
break;
|
||
case 'video':
|
||
dataArray = this.videoPrompts;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
|
||
// 找到要删除的规则索引
|
||
const index = dataArray.findIndex(p => p.id === promptId);
|
||
if (index === -1) return;
|
||
|
||
// 获取规则名称用于提示
|
||
const promptName = dataArray[index].name;
|
||
|
||
// 创建确认对话框(使用危险按钮样式)
|
||
createSettingsDialog({
|
||
title: `<i class="pi pi-exclamation-triangle" style="margin-right: 8px; color: var(--p-orange-500);"></i>${tUI('确认删除')}`,
|
||
isConfirmDialog: true,
|
||
dialogClassName: 'confirm-dialog',
|
||
disableBackdropAndCloseOnClickOutside: true,
|
||
saveButtonText: tUI('删除'),
|
||
saveButtonIcon: 'pi-trash',
|
||
isDangerButton: true,
|
||
cancelButtonText: tUI('取消'),
|
||
renderContent: (content) => {
|
||
content.style.textAlign = 'center';
|
||
content.style.padding = '1rem';
|
||
|
||
const confirmMessage = document.createElement('p');
|
||
confirmMessage.textContent = `${tUI('确定要删除规则')} "${promptName}" ${tUI('吗?')}`;
|
||
confirmMessage.style.margin = '0';
|
||
confirmMessage.style.fontSize = '1rem';
|
||
|
||
content.appendChild(confirmMessage);
|
||
},
|
||
onSave: () => {
|
||
// 从数组中删除规则
|
||
dataArray.splice(index, 1);
|
||
|
||
// 标记为已修改
|
||
this._setDirty();
|
||
|
||
// 重新渲染列表
|
||
this._renderPromptLists();
|
||
|
||
return true;
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
/**
|
||
* 将当前配置保存到服务器
|
||
* @returns {Promise} 保存操作的Promise
|
||
*/
|
||
async _saveConfigToServer() {
|
||
try {
|
||
// 获取原始版本号(从加载时保存的 systemPrompts 获取)
|
||
const configVersion = this.systemPrompts?.__config_version || '2.0';
|
||
|
||
// 构建系统规则配置(保留版本号)
|
||
const systemPrompts = {
|
||
__config_version: configVersion,
|
||
expand_prompts: {},
|
||
translate_prompts: {},
|
||
vision_prompts: {},
|
||
video_prompts: {},
|
||
active_prompts: {
|
||
expand: null,
|
||
vision_zh: null,
|
||
vision_en: null,
|
||
video: null
|
||
}
|
||
};
|
||
|
||
// 添加翻译规则(从 this.translatePrompts 动态构建)
|
||
(this.translatePrompts || []).forEach(prompt => {
|
||
systemPrompts.translate_prompts[prompt.id] = {
|
||
role: prompt.role || 'system',
|
||
content: prompt.content
|
||
};
|
||
});
|
||
|
||
|
||
// 创建一个有序的规则列表
|
||
const orderedExpandPrompts = [...this.expandPrompts].sort((a, b) => a.order - b.order);
|
||
|
||
// 添加扩写规则,并记录激活的规则ID
|
||
orderedExpandPrompts.forEach(prompt => {
|
||
systemPrompts.expand_prompts[prompt.id] = {
|
||
name: prompt.name,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
role: "system",
|
||
content: prompt.content
|
||
};
|
||
|
||
// 记录激活的规则ID
|
||
if (prompt.isActive) {
|
||
systemPrompts.active_prompts.expand = prompt.id;
|
||
logger.debug(`保存激活的提示词优化规则ID: ${prompt.id}`);
|
||
}
|
||
});
|
||
|
||
// 如果没有找到激活的扩写规则,使用实例属性中保存的ID
|
||
if (!systemPrompts.active_prompts.expand && this.activeExpandPromptId) {
|
||
systemPrompts.active_prompts.expand = this.activeExpandPromptId;
|
||
logger.debug(`使用实例属性中的提示词优化规则ID: ${this.activeExpandPromptId}`);
|
||
}
|
||
|
||
// 创建有序的中文和英文反推规则列表
|
||
const orderedZhVisionPrompts = [...this.zhVisionPrompts].sort((a, b) => a.order - b.order);
|
||
const orderedEnVisionPrompts = [...this.enVisionPrompts].sort((a, b) => a.order - b.order);
|
||
|
||
// 添加中文反推规则
|
||
orderedZhVisionPrompts.forEach(prompt => {
|
||
systemPrompts.vision_prompts[prompt.id] = {
|
||
name: prompt.name,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
role: "system",
|
||
content: prompt.content
|
||
};
|
||
|
||
// 记录激活的规则ID
|
||
if (prompt.isActive) {
|
||
systemPrompts.active_prompts.vision_zh = prompt.id;
|
||
logger.debug(`保存激活的中文反推规则ID: ${prompt.id}`);
|
||
}
|
||
});
|
||
|
||
// 如果没有找到激活的中文反推规则,使用实例属性中保存的ID
|
||
if (!systemPrompts.active_prompts.vision_zh && this.activeZhVisionPromptId) {
|
||
systemPrompts.active_prompts.vision_zh = this.activeZhVisionPromptId;
|
||
logger.debug(`使用实例属性中的中文反推规则ID: ${this.activeZhVisionPromptId}`);
|
||
}
|
||
|
||
// 添加英文反推规则
|
||
orderedEnVisionPrompts.forEach(prompt => {
|
||
systemPrompts.vision_prompts[prompt.id] = {
|
||
name: prompt.name,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
role: "system",
|
||
content: prompt.content
|
||
};
|
||
|
||
// 记录激活的规则ID
|
||
if (prompt.isActive) {
|
||
systemPrompts.active_prompts.vision_en = prompt.id;
|
||
logger.debug(`保存激活的英文反推规则ID: ${prompt.id}`);
|
||
}
|
||
});
|
||
|
||
// 如果没有找到激活的英文反推规则,使用实例属性中保存的ID
|
||
if (!systemPrompts.active_prompts.vision_en && this.activeEnVisionPromptId) {
|
||
systemPrompts.active_prompts.vision_en = this.activeEnVisionPromptId;
|
||
logger.debug(`使用实例属性中的英文反推规则ID: ${this.activeEnVisionPromptId}`);
|
||
}
|
||
|
||
// 添加视频反推规则
|
||
const orderedVideoPrompts = [...(this.videoPrompts || [])].sort((a, b) => a.order - b.order);
|
||
orderedVideoPrompts.forEach(prompt => {
|
||
systemPrompts.video_prompts[prompt.id] = {
|
||
name: prompt.name,
|
||
tags: prompt.tags || [],
|
||
category: prompt.category || '',
|
||
showIn: prompt.showIn || ['frontend', 'node'],
|
||
role: "system",
|
||
content: prompt.content
|
||
};
|
||
|
||
// 记录激活的规则ID
|
||
if (prompt.isActive) {
|
||
systemPrompts.active_prompts.video = prompt.id;
|
||
logger.debug(`保存激活的视频反推规则ID: ${prompt.id}`);
|
||
}
|
||
});
|
||
|
||
// 如果没有找到激活的视频反推规则,使用实例属性中保存的ID
|
||
if (!systemPrompts.active_prompts.video && this.activeVideoPromptId) {
|
||
systemPrompts.active_prompts.video = this.activeVideoPromptId;
|
||
logger.debug(`使用实例属性中的视频反推规则ID: ${this.activeVideoPromptId}`);
|
||
}
|
||
|
||
// 输出最终的激活状态
|
||
logger.debug(`最终激活的规则ID: expand=${systemPrompts.active_prompts.expand}, vision_zh=${systemPrompts.active_prompts.vision_zh}, vision_en=${systemPrompts.active_prompts.vision_en}, video=${systemPrompts.active_prompts.video}`);
|
||
|
||
|
||
const response = await fetch(APIService.getApiUrl('/config/system_prompts'), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(systemPrompts)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`保存失败: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
logger.debug('配置已成功保存到服务器');
|
||
return true;
|
||
} catch (error) {
|
||
logger.error(`保存系统规则配置失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为头部单元格添加列宽调整手柄
|
||
* @param {HTMLElement} headerRow 头部行元素
|
||
*/
|
||
_addHeaderResizers(headerRow) {
|
||
const cells = headerRow.querySelectorAll('.prompt-list-cell');
|
||
// 为前三列添加调整手柄(最后一列操作列不需要)
|
||
for (let i = 0; i < cells.length - 1; i++) {
|
||
const cell = cells[i];
|
||
const resizer = document.createElement('div');
|
||
resizer.className = 'column-resizer';
|
||
cell.appendChild(resizer);
|
||
this._addColumnResizer(resizer, cell);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加列宽调整功能
|
||
* @param {HTMLElement} resizer 调整手柄元素
|
||
* @param {HTMLElement} column 列元素
|
||
*/
|
||
_addColumnResizer(resizer, column) {
|
||
let isResizing = false;
|
||
let startX = 0;
|
||
let startWidth = 0;
|
||
|
||
resizer.addEventListener('mousedown', (e) => {
|
||
isResizing = true;
|
||
startX = e.clientX;
|
||
startWidth = parseInt(document.defaultView.getComputedStyle(column).width, 10);
|
||
|
||
// 添加拖拽状态样式
|
||
resizer.classList.add('resizing');
|
||
document.body.classList.add('column-resizing');
|
||
|
||
// 添加事件监听器
|
||
document.addEventListener('mousemove', handleMouseMove, { capture: true });
|
||
document.addEventListener('mouseup', handleMouseUp, { capture: true });
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
|
||
const handleMouseMove = (e) => {
|
||
if (!isResizing) return;
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
const deltaX = e.clientX - startX;
|
||
const newWidth = startWidth + deltaX;
|
||
|
||
// 设置最小和最大宽度限制
|
||
const minWidth = 80;
|
||
const maxWidth = 500;
|
||
|
||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||
// 获取列的类名以确定是哪一列
|
||
const columnClass = Array.from(column.classList).find(cls =>
|
||
cls.includes('-cell') && cls !== 'prompt-list-cell'
|
||
);
|
||
|
||
if (columnClass) {
|
||
// 同时调整所有相同类型的列
|
||
const allColumns = document.querySelectorAll(`.${columnClass}`);
|
||
allColumns.forEach(col => {
|
||
col.style.width = newWidth + 'px';
|
||
col.style.flexShrink = '0';
|
||
col.style.flexGrow = '0';
|
||
col.style.flexBasis = newWidth + 'px';
|
||
});
|
||
} else {
|
||
// 如果没有找到特定类名,只调整当前列
|
||
column.style.width = newWidth + 'px';
|
||
column.style.flexShrink = '0';
|
||
column.style.flexGrow = '0';
|
||
column.style.flexBasis = newWidth + 'px';
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleMouseUp = (e) => {
|
||
if (!isResizing) return;
|
||
|
||
isResizing = false;
|
||
|
||
// 获取列的类名以确定是哪一列
|
||
const columnClass = Array.from(column.classList).find(cls =>
|
||
cls.includes('-cell') && cls !== 'prompt-list-cell'
|
||
);
|
||
|
||
// 重置 flex 属性,允许下次调整
|
||
if (columnClass) {
|
||
const allColumns = document.querySelectorAll(`.${columnClass}`);
|
||
allColumns.forEach(col => {
|
||
col.style.flexShrink = '';
|
||
col.style.flexGrow = '';
|
||
col.style.flexBasis = '';
|
||
});
|
||
} else {
|
||
column.style.flexShrink = '';
|
||
column.style.flexGrow = '';
|
||
column.style.flexBasis = '';
|
||
}
|
||
|
||
// 移除拖拽状态样式
|
||
resizer.classList.remove('resizing');
|
||
document.body.classList.remove('column-resizing');
|
||
|
||
// 移除事件监听器
|
||
document.removeEventListener('mousemove', handleMouseMove, { capture: true });
|
||
document.removeEventListener('mouseup', handleMouseUp, { capture: true });
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
};
|
||
|
||
// 添加鼠标进入和离开事件以增强视觉反馈
|
||
resizer.addEventListener('mouseenter', () => {
|
||
if (!isResizing) {
|
||
resizer.style.opacity = '0.6';
|
||
}
|
||
});
|
||
|
||
resizer.addEventListener('mouseleave', () => {
|
||
if (!isResizing) {
|
||
resizer.style.opacity = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 初始化拖拽排序功能
|
||
* @param {HTMLElement} container 列表容器
|
||
* @param {string} type 类型
|
||
*/
|
||
_initSortable(container, type) {
|
||
// 检查是否已加载Sortable库
|
||
if (typeof Sortable === 'undefined') {
|
||
this._loadSortableLibrary().then(() => {
|
||
this._createSortableInstance(container, type);
|
||
}).catch(error => {
|
||
logger.error('加载Sortable库失败:', error);
|
||
});
|
||
} else {
|
||
this._createSortableInstance(container, type);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载Sortable库
|
||
* @returns {Promise}
|
||
*/
|
||
_loadSortableLibrary() {
|
||
return new Promise((resolve, reject) => {
|
||
if (typeof Sortable !== 'undefined') {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = '/custom_nodes/comfyui_prompt_assistant/js/lib/Sortable.min.js';
|
||
script.onload = () => resolve();
|
||
script.onerror = () => reject(new Error('Failed to load Sortable library'));
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建Sortable实例
|
||
* @param {HTMLElement} container 列表容器
|
||
* @param {string} type 类型
|
||
*/
|
||
_createSortableInstance(container, type) {
|
||
new Sortable(container, {
|
||
animation: 150,
|
||
ghostClass: 'sortable-ghost',
|
||
chosenClass: 'sortable-chosen',
|
||
dragClass: 'sortable-drag',
|
||
handle: '.prompt-list-row',
|
||
// 排除状态列和操作列,只允许从名称和内容列拖拽
|
||
filter: '.status-cell, .action-cell',
|
||
preventOnFilter: true,
|
||
onStart: (evt) => {
|
||
logger.debug(`开始拖拽: 类型=${type}, 索引=${evt.oldIndex}`);
|
||
},
|
||
onEnd: (evt) => {
|
||
logger.debug(`结束拖拽: 类型=${type}, 旧索引=${evt.oldIndex}, 新索引=${evt.newIndex}`);
|
||
this._handleSortEnd(evt, type);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理拖拽排序结束事件
|
||
* @param {Event} evt 排序事件
|
||
* @param {string} type 类型
|
||
*/
|
||
_handleSortEnd(evt, type) {
|
||
const { oldIndex, newIndex } = evt;
|
||
if (oldIndex === newIndex) return;
|
||
|
||
// 获取对应的数据数组
|
||
let dataArray;
|
||
switch (type) {
|
||
case 'expand':
|
||
dataArray = this.expandPrompts;
|
||
break;
|
||
case 'zhVision':
|
||
dataArray = this.zhVisionPrompts;
|
||
break;
|
||
case 'enVision':
|
||
dataArray = this.enVisionPrompts;
|
||
break;
|
||
case 'video':
|
||
dataArray = this.videoPrompts;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
|
||
// 输出排序前的顺序信息
|
||
logger.debug(`排序前数据: ${JSON.stringify(dataArray.map(item => ({ id: item.id, order: item.order })))}`);
|
||
|
||
// 重新排序数据
|
||
const movedItem = dataArray.splice(oldIndex, 1)[0];
|
||
dataArray.splice(newIndex, 0, movedItem);
|
||
|
||
// 更新所有项目的顺序属性
|
||
dataArray.forEach((item, index) => {
|
||
item.order = index;
|
||
});
|
||
|
||
// 输出排序后的顺序信息
|
||
logger.debug(`排序后数据: ${JSON.stringify(dataArray.map(item => ({ id: item.id, order: item.order })))}`);
|
||
logger.debug(`${type} 列表排序已更新: ${oldIndex} -> ${newIndex} | 移动项ID: ${movedItem.id}`);
|
||
|
||
// 标记为已修改
|
||
this._setDirty();
|
||
}
|
||
}
|
||
|
||
// 导出规则配置管理器实例
|
||
export const rulesConfigManager = new RulesConfigManager();
|