1370 lines
64 KiB
JavaScript
1370 lines
64 KiB
JavaScript
/**
|
||
* 小助手设置服务
|
||
* 负责管理小助手的设置选项,提供开关控制功能
|
||
*/
|
||
|
||
import { app } from "../../../../scripts/app.js";
|
||
import { logger } from '../utils/logger.js';
|
||
import { PromptAssistant } from "./PromptAssistant.js";
|
||
import { ImageCaption } from "./imageCaption.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 { APIService } from "../services/api.js";
|
||
import {
|
||
UI_LANGUAGE_SETTING_ID,
|
||
getStoredUiLanguage,
|
||
persistUiLanguage,
|
||
ensureUiLocaleLoaded,
|
||
tUI,
|
||
patchToastLocalization,
|
||
syncGlobalUiLocalization,
|
||
LANGUAGE_OPTIONS
|
||
} from "../utils/uiI18n.js";
|
||
|
||
import { apiConfigManager } from "./apiConfigManager.js";
|
||
import { rulesConfigManager } from "./rulesConfigManager.js";
|
||
import {
|
||
createSettingsDialog,
|
||
closeModalWithAnimation,
|
||
createFormGroup,
|
||
createInputGroup,
|
||
createSelectGroup,
|
||
createHorizontalFormGroup,
|
||
createLoadingButton
|
||
} from "./uiComponents.js";
|
||
|
||
// 标记是否是首次加载页面
|
||
let isFirstLoad = true;
|
||
|
||
function localizeSettingsPayload(payload) {
|
||
if (Array.isArray(payload)) {
|
||
return payload.map(item => localizeSettingsPayload(item));
|
||
}
|
||
|
||
if (!payload || typeof payload !== "object") {
|
||
return payload;
|
||
}
|
||
|
||
const localized = {};
|
||
for (const [key, value] of Object.entries(payload)) {
|
||
if (key === "category" && Array.isArray(value)) {
|
||
localized[key] = value.map(item => tUI(item, item));
|
||
continue;
|
||
}
|
||
|
||
if (["name", "tooltip", "text", "summary", "detail"].includes(key) && typeof value === "string") {
|
||
localized[key] = tUI(value, value);
|
||
continue;
|
||
}
|
||
|
||
if (typeof value === "function") {
|
||
localized[key] = value;
|
||
continue;
|
||
}
|
||
|
||
localized[key] = localizeSettingsPayload(value);
|
||
}
|
||
|
||
return localized;
|
||
}
|
||
|
||
// ---服务选择器配置---
|
||
const SERVICE_TYPES = {
|
||
translate: {
|
||
name: tUI('翻译'),
|
||
configEndpoint: '/config/translate',
|
||
serviceType: 'translate',
|
||
filterKey: 'llm_models',
|
||
includeBaidu: true
|
||
},
|
||
llm: {
|
||
name: tUI('提示词优化'),
|
||
configEndpoint: '/config/llm',
|
||
serviceType: 'llm',
|
||
filterKey: 'llm_models',
|
||
includeBaidu: false
|
||
},
|
||
vlm: {
|
||
name: tUI('图像反推'),
|
||
configEndpoint: '/config/vision',
|
||
serviceType: 'vlm',
|
||
filterKey: 'vlm_models',
|
||
includeBaidu: false
|
||
}
|
||
};
|
||
|
||
// ---服务选择器---
|
||
const serviceSelector = {
|
||
_servicesCache: null,
|
||
_cacheTime: 0,
|
||
_cacheDuration: 2000, // 缓存2秒
|
||
|
||
/**
|
||
* 清除服务缓存
|
||
*/
|
||
clearCache() {
|
||
this._servicesCache = null;
|
||
this._cacheTime = 0;
|
||
logger.debug('服务列表缓存已清除');
|
||
},
|
||
|
||
// 获取服务列表(带缓存)
|
||
async getServices(forceRefresh = false) {
|
||
const now = Date.now();
|
||
if (!forceRefresh && this._servicesCache && (now - this._cacheTime) < this._cacheDuration) {
|
||
return this._servicesCache;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(APIService.getApiUrl('/services'));
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this._servicesCache = data.services || [];
|
||
this._cacheTime = now;
|
||
return this._servicesCache;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logger.error(`获取服务列表失败: ${error.message}`);
|
||
}
|
||
return [];
|
||
},
|
||
|
||
// 获取指定类型的当前服务ID
|
||
async getCurrentService(type) {
|
||
const config = SERVICE_TYPES[type];
|
||
if (!config) return null;
|
||
|
||
try {
|
||
const response = await fetch(APIService.getApiUrl(config.configEndpoint));
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
return data.provider || null;
|
||
}
|
||
} catch (error) {
|
||
logger.error(`获取${config.name}当前服务失败: ${error.message}`);
|
||
}
|
||
return null;
|
||
},
|
||
|
||
// 设置指定类型的服务
|
||
async setCurrentService(type, serviceId) {
|
||
const config = SERVICE_TYPES[type];
|
||
if (!config) return false;
|
||
|
||
try {
|
||
const response = await fetch(APIService.getApiUrl('/services/current'), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
service_type: config.serviceType,
|
||
service_id: serviceId
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
logger.log(`${config.name}服务切换 | 服务ID: ${serviceId}`);
|
||
|
||
// 派发全局事件通知其他组件同步
|
||
window.dispatchEvent(new CustomEvent('pa-service-changed', {
|
||
detail: { service_type: config.serviceType, service_id: serviceId }
|
||
}));
|
||
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
logger.error(`切换${config.name}服务失败: ${error.message}`);
|
||
}
|
||
return false;
|
||
},
|
||
|
||
// 获取指定类型可用的服务选项列表
|
||
async getServiceOptions(type) {
|
||
const config = SERVICE_TYPES[type];
|
||
if (!config) return [];
|
||
|
||
const services = await this.getServices();
|
||
const options = [];
|
||
|
||
// 添加百度翻译选项(仅翻译类型)
|
||
if (config.includeBaidu) {
|
||
options.push({ value: 'baidu', text: tUI('百度翻译') });
|
||
}
|
||
|
||
// 过滤并添加其他服务
|
||
services
|
||
.filter(service => {
|
||
const models = service[config.filterKey];
|
||
return models && models.length > 0;
|
||
})
|
||
.forEach(service => {
|
||
options.push({
|
||
value: service.id,
|
||
text: service.name || service.id
|
||
});
|
||
});
|
||
|
||
return options;
|
||
}
|
||
};
|
||
|
||
// 将服务选择器挂载到全局 app 对象,方便其他模块(如 PromptAssistant.js, imageCaption.js)调用,
|
||
// 同时避免模块间的循环引用问题。
|
||
app.paServiceSelector = serviceSelector;
|
||
|
||
// ---版本检查工具函数---
|
||
|
||
// 版本检查状态缓存
|
||
let versionCheckCache = {
|
||
checked: false, // 是否已检查过
|
||
latestVersion: null, // 最新版本号
|
||
hasUpdate: false // 是否有更新
|
||
};
|
||
|
||
/**
|
||
* 从 jsDelivr 获取最新版本号(通过读取 pyproject.toml)
|
||
* @returns {Promise<string|null>} 返回最新版本号,格式如 "1.2.3",失败返回 null
|
||
*/
|
||
async function fetchLatestVersion() {
|
||
// 如果已经检查过,直接返回缓存结果
|
||
if (versionCheckCache.checked) {
|
||
return versionCheckCache.latestVersion;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('https://cdn.jsdelivr.net/gh/yawiii/ComfyUI-Prompt-Assistant@main/pyproject.toml', {
|
||
cache: 'no-cache'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
logger.warn(`[版本检查] 请求失败: ${response.status}`);
|
||
versionCheckCache.checked = true;
|
||
return null;
|
||
}
|
||
|
||
const tomlContent = await response.text();
|
||
const versionMatch = tomlContent.match(/^version\s*=\s*["']([^"']+)["']/m);
|
||
const version = versionMatch ? versionMatch[1] : null;
|
||
|
||
// 缓存检查结果
|
||
versionCheckCache.checked = true;
|
||
versionCheckCache.latestVersion = version;
|
||
|
||
return version;
|
||
} catch (error) {
|
||
logger.warn(`[版本检查] 获取失败: ${error.message}`);
|
||
versionCheckCache.checked = true;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 比较两个版本号
|
||
* @param {string} v1 - 第一个版本号
|
||
* @param {string} v2 - 第二个版本号
|
||
* @returns {number} v1 > v2 返回 1,v1 < v2 返回 -1,v1 === v2 返回 0
|
||
*/
|
||
function compareVersion(v1, v2) {
|
||
// 将版本号分割为数字数组
|
||
const parts1 = v1.split('.').map(n => parseInt(n, 10) || 0);
|
||
const parts2 = v2.split('.').map(n => parseInt(n, 10) || 0);
|
||
|
||
// 确保两个数组长度相同
|
||
const maxLength = Math.max(parts1.length, parts2.length);
|
||
|
||
for (let i = 0; i < maxLength; i++) {
|
||
const num1 = parts1[i] || 0;
|
||
const num2 = parts2[i] || 0;
|
||
|
||
if (num1 > num2) return 1;
|
||
if (num1 < num2) return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
// ====================== 设置管理 ======================
|
||
|
||
/**
|
||
* 显示API配置弹窗
|
||
*/
|
||
function showAPIConfigModal() {
|
||
try {
|
||
// 调用API配置管理器的显示弹窗方法
|
||
apiConfigManager.showAPIConfigModal();
|
||
} catch (error) {
|
||
logger.error(`打开API配置弹窗失败: ${error.message}`);
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: tUI("打开配置失败"),
|
||
detail: error.message || tUI("打开配置弹窗过程中发生错误"),
|
||
life: 3000
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示规则配置弹窗
|
||
*/
|
||
function showRulesConfigModal() {
|
||
try {
|
||
// 调用规则配置管理器的显示弹窗方法
|
||
rulesConfigManager.showRulesConfigModal();
|
||
} catch (error) {
|
||
logger.error(`打开规则配置弹窗失败: ${error.message}`);
|
||
app.extensionManager.toast.add({
|
||
severity: "error",
|
||
summary: tUI("打开配置失败"),
|
||
detail: error.message || tUI("打开配置弹窗过程中发生错误"),
|
||
life: 3000
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建服务选择器下拉框
|
||
* @param {string} type - 服务类型: 'translate' | 'llm' | 'vlm'
|
||
* @param {string} label - 显示名称
|
||
* @returns {HTMLElement} 设置行元素
|
||
*/
|
||
function createServiceSelector(type, label) {
|
||
const row = document.createElement("tr");
|
||
row.className = "promptwidget-settings-row";
|
||
|
||
const labelCell = document.createElement("td");
|
||
labelCell.className = "comfy-menu-label";
|
||
row.appendChild(labelCell);
|
||
|
||
const selectCell = document.createElement("td");
|
||
|
||
// 创建加载占位容器
|
||
const container = document.createElement("div");
|
||
container.style.minWidth = "180px";
|
||
container.innerHTML = `<span style="color: var(--p-text-muted-color); font-size: 12px;">${tUI("加载中...")}</span>`;
|
||
|
||
selectCell.appendChild(container);
|
||
row.appendChild(selectCell);
|
||
|
||
let currentOptions = []; // 存储当前选项引用
|
||
let updateDropdownOptions = null; // 存储更新函数
|
||
|
||
/**
|
||
* 更新下拉框内容
|
||
* @param {boolean} force - 是否强制刷新数据
|
||
*/
|
||
const updateContent = async (force = false) => {
|
||
try {
|
||
if (force) {
|
||
// 如果是强制刷新(如配置变更或点击触发),先清除缓存
|
||
serviceSelector.clearCache();
|
||
}
|
||
|
||
// 获取服务列表和当前选中的服务
|
||
const [options, currentService] = await Promise.all([
|
||
serviceSelector.getServiceOptions(type),
|
||
serviceSelector.getCurrentService(type)
|
||
]);
|
||
|
||
// 如果已经存在下拉框实例,则尝试增量更新
|
||
if (updateDropdownOptions) {
|
||
updateDropdownOptions(options, currentService);
|
||
currentOptions = options;
|
||
return;
|
||
}
|
||
|
||
// ---首次加载逻辑---
|
||
container.innerHTML = '';
|
||
|
||
if (options.length === 0) {
|
||
container.innerHTML = `<span style="color: var(--p-text-muted-color); font-size: 12px;">${tUI("暂无可用服务")}</span>`;
|
||
return;
|
||
}
|
||
|
||
currentOptions = options;
|
||
const res = createSelectGroup(label, options, currentService, { showLabel: false });
|
||
const { group, select } = res;
|
||
updateDropdownOptions = res.updateOptions;
|
||
|
||
// 将 group 的子元素添加到容器
|
||
while (group.firstChild) {
|
||
container.appendChild(group.firstChild);
|
||
}
|
||
|
||
// 监听点击/按下事件:当用户准备点击下拉框时,尝试静默同步最新配置
|
||
const dropdownContainer = container.querySelector('.pa-dropdown');
|
||
if (dropdownContainer) {
|
||
dropdownContainer.addEventListener('mousedown', () => {
|
||
// 点击时触发刷新,但不显示“同步中”以避免干扰 UI
|
||
updateContent(true);
|
||
});
|
||
}
|
||
|
||
// 监听变更事件
|
||
select.addEventListener('change', async () => {
|
||
const newValue = select.value;
|
||
if (!newValue) return;
|
||
|
||
const dropdown = container.querySelector('.pa-dropdown');
|
||
if (dropdown) {
|
||
dropdown.style.opacity = '0.6';
|
||
dropdown.style.pointerEvents = 'none';
|
||
}
|
||
|
||
try {
|
||
const success = await serviceSelector.setCurrentService(type, newValue);
|
||
if (success) {
|
||
logger.log(`设置${label}服务 | 服务: ${newValue}`);
|
||
} else {
|
||
logger.error(`设置${label}服务失败`);
|
||
const oldValue = await serviceSelector.getCurrentService(type);
|
||
if (oldValue && updateDropdownOptions) {
|
||
updateDropdownOptions(currentOptions, oldValue);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logger.error(`设置${label}服务异常: ${error.message}`);
|
||
} finally {
|
||
if (dropdown) {
|
||
dropdown.style.opacity = '';
|
||
dropdown.style.pointerEvents = '';
|
||
}
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
logger.error(`同步${label}配置失败: ${error.message}`);
|
||
if (!updateDropdownOptions) {
|
||
container.innerHTML = `<span style="color: var(--p-red-400); font-size: 12px;">${tUI("加载失败")}</span>`;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 初始加载
|
||
updateContent();
|
||
|
||
// 监听配置更新事件(当 API 配置管理器修改配置后触发)
|
||
const onConfigUpdated = () => {
|
||
logger.debug(`收到配置更新通知,同步${label}状态...`);
|
||
updateContent(true);
|
||
};
|
||
window.addEventListener('pa-config-updated', onConfigUpdated);
|
||
|
||
// 销毁监听器的清理函数(简单处理,因为设置面板通常随页面销毁)
|
||
// 如果之后有更复杂的组件挂载逻辑,可以在这里返回一个清理函数给外部调用
|
||
|
||
return row;
|
||
}
|
||
|
||
|
||
/**
|
||
* 注册设置选项
|
||
* 将设置选项添加到ComfyUI设置面板
|
||
*/
|
||
export async function registerSettings() {
|
||
try {
|
||
await ensureUiLocaleLoaded();
|
||
patchToastLocalization();
|
||
syncGlobalUiLocalization();
|
||
|
||
app.registerExtension({
|
||
name: "PromptAssistant.Settings",
|
||
settings: localizeSettingsPayload([
|
||
{
|
||
id: UI_LANGUAGE_SETTING_ID,
|
||
name: "界面语言",
|
||
category: ["✨提示词小助手", "系统", "界面语言"],
|
||
type: "combo",
|
||
options: LANGUAGE_OPTIONS,
|
||
defaultValue: "zh",
|
||
tooltip: "切换插件界面语言,修改后自动刷新页面生效",
|
||
onChange: (value) => {
|
||
const previous = getStoredUiLanguage();
|
||
const normalized = persistUiLanguage(value);
|
||
if (normalized === previous) return;
|
||
logger.log(`界面语言已切换: ${normalized}`);
|
||
setTimeout(() => window.location.reload(), 80);
|
||
}
|
||
},
|
||
// 总开关 - 独立控制小助手系统级功能
|
||
{
|
||
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: tUI("提示词小助手已启用"),
|
||
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: tUI("提示词小助手已禁用"),
|
||
life: 3000
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 设置首次加载标志为 false,表示已经完成首次加载
|
||
isFirstLoad = false;
|
||
} catch (error) {
|
||
logger.error(`总开关切换异常 | 错误:${error.message}`);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 小助手创建方式设置
|
||
{
|
||
id: "PromptAssistant.Settings.CreationMode",
|
||
name: "小助手创建方式(提示词)",
|
||
category: ["✨提示词小助手", "系统", "提示词小助手创建方式"],
|
||
type: "combo",
|
||
options: [
|
||
{ text: "自动创建", value: "auto" },
|
||
{ text: "选中节点时创建", value: "manual" }
|
||
],
|
||
defaultValue: "auto",
|
||
tooltip: "自动创建:节点创建或加载时自动显示小助手;选中节点时创建:仅选中节点时显示",
|
||
onChange: (value) => {
|
||
logger.log(`小助手创建方式变更 | 模式:${value === 'auto' ? '自动创建' : '选中节点时创建'}`);
|
||
// 如果切换到自动创建,立即尝试初始化所有节点
|
||
if (value === 'auto' && window.FEATURES.enabled && app.graph) {
|
||
const nodes = app.graph._nodes || [];
|
||
nodes.forEach(node => {
|
||
if (node && !node._promptAssistantInitialized) {
|
||
app.promptAssistant.checkAndSetupNode(node);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
// 反推小助手创建方式设置
|
||
{
|
||
id: "PromptAssistant.Settings.ImageCaptionCreationMode",
|
||
name: "小助手创建方式(图像反推)",
|
||
category: ["✨提示词小助手", "系统", "图像小助手创建方式"],
|
||
type: "combo",
|
||
options: [
|
||
{ text: "自动创建", value: "auto" },
|
||
{ text: "选中节点时创建", value: "manual" }
|
||
],
|
||
defaultValue: "auto",
|
||
tooltip: "自动创建:节点创建或加载时自动显示反推小助手;选中节点时创建:仅选中节点时显示",
|
||
onChange: (value) => {
|
||
logger.log(`反推小助手创建方式变更 | 模式:${value === 'auto' ? '自动创建' : '选中节点时创建'}`);
|
||
// 如果切换到自动创建,立即尝试初始化所有节点
|
||
if (value === 'auto' && window.FEATURES.enabled && window.FEATURES.imageCaption && app.graph) {
|
||
const nodes = app.graph._nodes || [];
|
||
nodes.forEach(node => {
|
||
if (node && !node._imageCaptionInitialized) {
|
||
app.imageCaption.checkAndSetupNode(node);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
// 小助手布局(提示词)
|
||
{
|
||
id: "PromptAssistant.Location",
|
||
name: "小助手布局(提示词)",
|
||
category: ["✨提示词小助手", "界面", "提示词小助手布局"],
|
||
type: "combo",
|
||
options: [
|
||
// { text: "左上(横向)", value: "top-left-h" },
|
||
// { text: "左上(垂直)", value: "top-left-v" },
|
||
// { text: "中上(横向)", value: "top-center-h" },
|
||
// { text: "⇗ ━", value: "top-right-h" },
|
||
// { text: "⇗ ┃", value: "top-right-v" },
|
||
{ text: "右中(垂直)", value: "right-center-v" },
|
||
{ text: "右下(横向)", value: "bottom-right-h" },
|
||
{ text: "右下(垂直)", value: "bottom-right-v" },
|
||
{ text: "下中(横向)", value: "bottom-center-h" },
|
||
{ text: "左下(横向)", value: "bottom-left-h" },
|
||
// { text: "左下(垂直)", value: "bottom-left-v" },
|
||
// { text: "左中(垂直)", value: "left-center-v" }
|
||
],
|
||
defaultValue: "bottom-right-h", // 默认右下横向
|
||
tooltip: "设置提示词小助手在输入框周围的布局和展开方向",
|
||
onChange: (value) => {
|
||
logger.log(`提示词小助手布局变更 | 布局:${value}`);
|
||
// 通知所有实例更新布局(通过 CSS 类处理)
|
||
PromptAssistant.instances.forEach(widget => {
|
||
if (widget.container && widget.container.setAnchorPosition) {
|
||
widget.container.setAnchorPosition(value);
|
||
}
|
||
});
|
||
}
|
||
},
|
||
// 小助手位置设置(图像反推)
|
||
{
|
||
id: "ImageCaption.Location",
|
||
name: "小助手布局(图像反推)",
|
||
category: ["✨提示词小助手", "界面", "图像小助手布局"],
|
||
type: "combo",
|
||
options: [
|
||
{ text: "横", value: "bottom-left-h" },
|
||
{ text: "竖", value: "bottom-left-v" }
|
||
],
|
||
defaultValue: "bottom-left-h", // 默认横向
|
||
tooltip: "设置图像反推小助手的展开方向(位置固定在左下角)",
|
||
onChange: (value) => {
|
||
logger.log(`图像反推小助手布局变更 | 布局:${value}`);
|
||
// 通知所有实例更新布局
|
||
ImageCaption.instances.forEach(assistant => {
|
||
if (assistant.container && assistant.container.setAnchorPosition) {
|
||
assistant.container.setAnchorPosition(value);
|
||
}
|
||
});
|
||
},
|
||
},
|
||
|
||
// 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(tUI("API管理器"), async () => {
|
||
showAPIConfigModal();
|
||
}, false); // 设置 showSuccessToast 为 false
|
||
|
||
buttonCell.appendChild(button);
|
||
row.appendChild(buttonCell);
|
||
return row;
|
||
}
|
||
},
|
||
|
||
// ---服务类别设置---
|
||
// 翻译服务选择
|
||
{
|
||
id: "PromptAssistant.Service.Translate",
|
||
name: "选择翻译服务",
|
||
category: ["✨提示词小助手", " 配置", "翻译"],
|
||
tooltip: "选择一个服务商用于翻译,也可以通过右键翻译按钮来切换",
|
||
type: () => {
|
||
return createServiceSelector('translate', '翻译');
|
||
}
|
||
},
|
||
|
||
// 提示词优化服务选择
|
||
{
|
||
id: "PromptAssistant.Service.LLM",
|
||
name: "选择提示词优化服务",
|
||
category: ["✨提示词小助手", " 配置", "提示词优化"],
|
||
tooltip: "选择一个服务商用于提示词优化,也可以通过右键提示词优化按钮来切换",
|
||
type: () => {
|
||
return createServiceSelector('llm', '提示词优化');
|
||
}
|
||
},
|
||
|
||
// 图像反推服务选择
|
||
{
|
||
id: "PromptAssistant.Service.VLM",
|
||
name: "选择图像反推服务",
|
||
category: ["✨提示词小助手", " 配置", "图像反推"],
|
||
tooltip: "选择一个服务商用于图像反推,也可以通过右键反推按钮来切换",
|
||
type: () => {
|
||
return createServiceSelector('vlm', '图像反推');
|
||
}
|
||
},
|
||
|
||
// 历史功能(包含历史、撤销、重做按钮)
|
||
{
|
||
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.CacheMixedLangTranslation",
|
||
name: "混合语言翻译进行缓存",
|
||
category: ["✨提示词小助手", " 翻译功能设置", "混合语言缓存"],
|
||
type: "boolean",
|
||
defaultValue: false,
|
||
tooltip: "关闭时,中英文混合内容的翻译结果不会写入缓存,避免污染缓存。开启后会正常缓存。",
|
||
onChange: (value) => {
|
||
FEATURES.cacheMixedLangTranslation = value;
|
||
logger.log(`混合语言缓存 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
|
||
// 混合语言翻译规则
|
||
{
|
||
id: "PromptAssistant.Features.MixedLangTranslateRule",
|
||
name: "混合语言翻译规则",
|
||
category: ["✨提示词小助手", " 翻译功能设置", "混合语言规则"],
|
||
type: "combo",
|
||
options: [
|
||
{ text: "翻译成英文", value: "to_en" },
|
||
{ text: "翻译成中文", value: "to_zh" },
|
||
{ text: "自动翻译小比例语言", value: "auto_minor" },
|
||
{ text: "自动翻译大比例语言", value: "auto_major" }
|
||
],
|
||
defaultValue: "to_en",
|
||
tooltip: "根据个人使用偏好设置混合中英文内容的翻译规则",
|
||
onChange: (value) => {
|
||
FEATURES.mixedLangTranslateRule = value;
|
||
logger.log(`混合语言翻译规则 - 已设置为:${value}`);
|
||
}
|
||
},
|
||
|
||
// 翻译格式化选项
|
||
{
|
||
id: "PromptAssistant.Features.TranslateFormatPunctuation",
|
||
name: "始终使用半角标点符号",
|
||
category: ["✨提示词小助手", " 翻译功能设置", "标点处理"],
|
||
type: "boolean",
|
||
defaultValue: false,
|
||
tooltip: "打开后,翻译结果会自动将中文标点符号替换成英文标点符号",
|
||
onChange: (value) => {
|
||
FEATURES.translateFormatPunctuation = value;
|
||
logger.log(`标点符号转换 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
{
|
||
id: "PromptAssistant.Features.TranslateFormatSpace",
|
||
name: "自动移除多余空格",
|
||
category: ["✨提示词小助手", " 翻译功能设置", "空格处理"],
|
||
type: "boolean",
|
||
defaultValue: false,
|
||
tooltip: "打开后,翻译结果会自动移除多余空格",
|
||
onChange: (value) => {
|
||
FEATURES.translateFormatSpace = value;
|
||
logger.log(`移除多余空格 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
{
|
||
id: "PromptAssistant.Features.TranslateFormatDots",
|
||
name: "移除多余连续点号",
|
||
category: ["✨提示词小助手", " 翻译功能设置", "点号处理"],
|
||
type: "boolean",
|
||
defaultValue: false,
|
||
tooltip: "打开后,翻译结果会将多余的“......”统一为“...”",
|
||
onChange: (value) => {
|
||
FEATURES.translateFormatDots = value;
|
||
logger.log(`处理连续点号 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
{
|
||
id: "PromptAssistant.Features.TranslateFormatNewline",
|
||
name: "保留换行符",
|
||
category: ["✨提示词小助手", " 翻译功能设置", "换行处理"],
|
||
type: "boolean",
|
||
defaultValue: true,
|
||
tooltip: "打开后,翻译结果会尽量保持原文的换行,避免翻译后丢失段落",
|
||
onChange: (value) => {
|
||
FEATURES.translateFormatNewline = 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.Features.NodeHelpTranslator",
|
||
name: "启用节点信息翻译",
|
||
category: ["✨提示词小助手", "小助手功能开关", "节点信息翻译"],
|
||
type: "boolean",
|
||
defaultValue: true,
|
||
tooltip: "开启或关闭ComfyUI侧边栏节点帮助文档的翻译功能",
|
||
onChange: (value) => {
|
||
const oldValue = FEATURES.nodeHelpTranslator;
|
||
FEATURES.nodeHelpTranslator = value;
|
||
handleFeatureChange('节点信息翻译', value, oldValue);
|
||
logger.log(`节点信息翻译功能 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
// 系统设置
|
||
{
|
||
id: "PromptAssistant.Settings.LogLevel",
|
||
name: "日志级别",
|
||
category: ["✨提示词小助手", "系统", "日志级别"],
|
||
type: "hidden",
|
||
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.ShowStreamingProgress",
|
||
name: "控制台流式输出进度日志",
|
||
category: ["✨提示词小助手", "系统", "终端日志"],
|
||
type: "boolean",
|
||
defaultValue: false,
|
||
tooltip: "开启后,控制台会显示流式输出过程,在某些终端可能导致刷屏;关闭后只显示静态的'生成中...'。",
|
||
onChange: async (value) => {
|
||
FEATURES.showStreamingProgress = value;
|
||
// 通知后端更新设置
|
||
try {
|
||
await fetch(APIService.getApiUrl('/settings/streaming_progress'), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enabled: value })
|
||
});
|
||
} catch (error) {
|
||
logger.error(`更新流式进度设置失败: ${error.message}`);
|
||
}
|
||
logger.log(`流式输出进度 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
|
||
// 流式输出开关
|
||
{
|
||
id: "PromptAssistant.Settings.EnableStreaming",
|
||
name: "流式输出开关",
|
||
category: ["✨提示词小助手", "系统", "流式体验"],
|
||
type: "boolean",
|
||
defaultValue: true,
|
||
tooltip: "开启时,翻译、扩写、识别等功能将以逐字生成的流式效果显示;关闭时则恢复为全部生成后一次性显示的阻塞模式。",
|
||
onChange: (value) => {
|
||
FEATURES.enableStreaming = value;
|
||
logger.log(`流式输出开关 - 已${value ? "启用" : "禁用"}`);
|
||
}
|
||
},
|
||
|
||
{
|
||
id: "PromptAssistant.Settings.IconOpacity",
|
||
name: " 小助手图标不透明度",
|
||
category: ["✨提示词小助手", "界面", "小助手图标"],
|
||
type: "slider",
|
||
min: 0,
|
||
max: 100,
|
||
step: 1,
|
||
defaultValue: 20,
|
||
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(tUI("清理所有缓存"), async () => {
|
||
try {
|
||
// 获取清理前的缓存统计
|
||
const beforeStats = {
|
||
history: HistoryCacheService.getHistoryStats(),
|
||
tags: 0,
|
||
translate: TranslateCacheService.getTranslateCacheStats(),
|
||
nodeHelpTranslate: 0 // 节点文档翻译缓存
|
||
};
|
||
|
||
// 统计所有标签数量
|
||
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) {
|
||
// 移除错误日志,静默处理解析错误
|
||
}
|
||
});
|
||
|
||
// 统计节点文档翻译缓存数量
|
||
try {
|
||
const nodeHelpCache = sessionStorage.getItem('pa_node_help_translations');
|
||
if (nodeHelpCache) {
|
||
const parsed = JSON.parse(nodeHelpCache);
|
||
beforeStats.nodeHelpTranslate = Object.keys(parsed).length;
|
||
}
|
||
} catch (e) {
|
||
// 静默处理
|
||
}
|
||
|
||
// 执行历史记录清理操作
|
||
HistoryCacheService.clearAllHistory();
|
||
|
||
// 清理所有标签缓存
|
||
TagCacheService.clearAllTagCache();
|
||
|
||
// 清理翻译缓存
|
||
TranslateCacheService.clearAllTranslateCache();
|
||
|
||
// 清理节点文档翻译缓存(sessionStorage)
|
||
sessionStorage.removeItem('pa_node_help_translations');
|
||
|
||
// 清理旧版本的标签缓存(以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;
|
||
const clearedNodeHelp = beforeStats.nodeHelpTranslate;
|
||
|
||
// 只输出最终统计结果
|
||
logger.log(`缓存清理完成 | 历史记录: ${clearedHistory}条 | 标签: ${clearedTags}个 | 翻译: ${clearedTranslate}条 | 节点文档: ${clearedNodeHelp}个`);
|
||
|
||
// 更新所有实例的撤销/重做按钮状态
|
||
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.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 versionLink = document.createElement("a");
|
||
versionLink.href = "https://github.com/yawiii/ComfyUI-Prompt-Assistant/releases/latest";
|
||
versionLink.target = "_blank";
|
||
versionLink.style.textDecoration = "none";
|
||
versionLink.style.display = "flex";
|
||
versionLink.style.alignItems = "center";
|
||
versionLink.style.cursor = "pointer";
|
||
|
||
const versionContainer = document.createElement("div");
|
||
versionContainer.style.display = "flex";
|
||
versionContainer.style.alignItems = "center";
|
||
versionContainer.style.gap = "8px";
|
||
versionLink.appendChild(versionContainer);
|
||
|
||
// 版本徽标
|
||
const versionBadge = document.createElement("img");
|
||
versionBadge.alt = "Version";
|
||
versionBadge.style.display = "block";
|
||
versionBadge.style.height = "20px";
|
||
|
||
// 从全局变量获取版本号
|
||
if (!window.PromptAssistant_Version) {
|
||
logger.error(tUI("未找到版本号,徽标将无法正确显示"));
|
||
versionBadge.src = `https://img.shields.io/badge/%E7%89%88%E6%9C%AC-%E6%9C%AA%E7%9F%A5-red?style=flat`;
|
||
versionContainer.appendChild(versionBadge);
|
||
} else {
|
||
const currentVersion = window.PromptAssistant_Version;
|
||
versionBadge.src = `https://img.shields.io/badge/%E7%89%88%E6%9C%AC-${currentVersion}-green?style=flat`;
|
||
versionContainer.appendChild(versionBadge);
|
||
|
||
// 使用缓存检查版本,避免重复请求
|
||
if (versionCheckCache.checked && versionCheckCache.hasUpdate) {
|
||
// 已检查过且有更新,直接应用缓存的结果
|
||
const latestVersion = versionCheckCache.latestVersion;
|
||
const labelEncoded = encodeURIComponent(tUI("有新版本"));
|
||
const messageEncoded = encodeURIComponent(`${currentVersion}→${latestVersion}`);
|
||
versionBadge.src = `https://img.shields.io/badge/${labelEncoded}-${messageEncoded}-orange?style=flat&labelColor=555555`;
|
||
versionBadge.style.cursor = "pointer";
|
||
versionBadge.title = `${tUI("当前版本:")} ${currentVersion}\n${tUI("最新版本:")} ${latestVersion}\n${tUI("点击前往下载")}`;
|
||
} else if (!versionCheckCache.checked) {
|
||
// 首次检查,发起异步请求
|
||
fetchLatestVersion().then(latestVersion => {
|
||
if (latestVersion && compareVersion(latestVersion, currentVersion) > 0) {
|
||
versionCheckCache.hasUpdate = true;
|
||
const labelEncoded = encodeURIComponent(tUI("有新版本"));
|
||
const messageEncoded = encodeURIComponent(`${currentVersion}→${latestVersion}`);
|
||
versionBadge.src = `https://img.shields.io/badge/${labelEncoded}-${messageEncoded}-orange?style=flat&labelColor=555555`;
|
||
versionBadge.style.cursor = "pointer";
|
||
versionBadge.title = `${tUI("当前版本:")} ${currentVersion}\n${tUI("最新版本:")} ${latestVersion}\n${tUI("点击前往下载")}`;
|
||
logger.log(`[版本检查] 发现新版本: ${currentVersion} → ${latestVersion}`);
|
||
} else if (latestVersion) {
|
||
versionBadge.title = `${tUI("当前已是最新版本:")} ${currentVersion}`;
|
||
logger.debug(`[版本检查] 当前版本: ${currentVersion}`);
|
||
}
|
||
}).catch(error => {
|
||
logger.warn(`[版本检查] 出错: ${error.message}`);
|
||
});
|
||
} else {
|
||
// 已检查过但没有更新
|
||
versionBadge.title = `${tUI("当前已是最新版本:")} ${currentVersion}`;
|
||
}
|
||
}
|
||
|
||
cell.appendChild(versionLink);
|
||
|
||
// 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/github/stars/yawiii/ComfyUI-Prompt-Assistant?style=flat&logo=github&logoColor=%23292F34&label=Yawiii&labelColor=%23FFFFFF&color=blue";
|
||
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/%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 = 'javascript:void(0)';
|
||
wechatTag.addEventListener('click', (e) => { e.preventDefault(); toggleWechatQr(); });
|
||
wechatTag.style.textDecoration = "none";
|
||
wechatTag.style.display = "flex";
|
||
wechatTag.style.alignItems = "center";
|
||
wechatTag.classList.add("has-tooltip", "pa-wechat-badge");
|
||
const wechatBadge = document.createElement("img");
|
||
wechatBadge.alt = tUI("交流反馈群");
|
||
wechatBadge.src = "https://img.shields.io/badge/%E4%BA%A4%E6%B5%81%E5%8F%8D%E9%A6%88-blue?logo=wechat&logoColor=green&labelColor=%23FFFFFF&color=%2307A3D7";
|
||
wechatBadge.style.display = "block";
|
||
wechatBadge.style.height = "20px";
|
||
wechatTag.appendChild(wechatBadge);
|
||
|
||
// 悬浮显示二维码
|
||
const wechatQr = document.createElement("div");
|
||
wechatQr.className = "pa-wechat-qr";
|
||
const wechatQrImg = document.createElement("img");
|
||
// 优先加载远程二维码,失败则回退到本地备用图
|
||
const remoteQrUrl = 'http://data.xflow.cc/wechat.png';
|
||
let qrFallbackTimer = null;
|
||
const localQrUrl = ResourceManager.getAssetUrl('wechat.png');
|
||
|
||
// 每次显示时强制重新加载远程二维码(带时间戳),避免缓存
|
||
const loadWechatQr = () => {
|
||
if (qrFallbackTimer) { clearTimeout(qrFallbackTimer); qrFallbackTimer = null; }
|
||
wechatQrImg.dataset.fallbackApplied = '';
|
||
wechatQrImg.dataset.source = 'remote';
|
||
wechatQrImg.src = `${remoteQrUrl}?t=${Date.now()}`;
|
||
// 超时回退到本地,但需要判断图片是否已开始加载
|
||
qrFallbackTimer = setTimeout(() => {
|
||
// 检查是否已标记为已回退
|
||
if (wechatQrImg.dataset.fallbackApplied === '1') return;
|
||
|
||
// 检查图片是否已开始加载(naturalHeight > 0 说明图片正在加载)
|
||
if (wechatQrImg.naturalHeight > 0) {
|
||
Logger.log(2, '远程二维码加载中,延长等待时间');
|
||
// 图片已开始加载,继续等待 onload,取消超时回退
|
||
if (qrFallbackTimer) { clearTimeout(qrFallbackTimer); qrFallbackTimer = null; }
|
||
} else {
|
||
// 图片未开始加载,可能是网络问题,回退到本地
|
||
Logger.log(1, '远程二维码加载超时,切换到本地备用图');
|
||
loadLocalQr();
|
||
}
|
||
}, 3000); // 延长到 3 秒,给远程图片更多加载时间
|
||
};
|
||
// 手动切换到本地二维码(带时间戳),清理超时
|
||
const loadLocalQr = () => {
|
||
if (qrFallbackTimer) { clearTimeout(qrFallbackTimer); qrFallbackTimer = null; }
|
||
wechatQrImg.dataset.fallbackApplied = '1';
|
||
wechatQrImg.dataset.source = 'local';
|
||
wechatQrImg.src = localQrUrl; // 本地图片固定,不加时间戳
|
||
};
|
||
|
||
// 点击徽标时在远程/本地之间来回切换
|
||
const toggleWechatQr = () => {
|
||
if (wechatQrImg.dataset.source === 'local') {
|
||
loadWechatQr();
|
||
} else {
|
||
loadLocalQr();
|
||
}
|
||
};
|
||
|
||
|
||
wechatQrImg.alt = tUI("微信交流群二维码");
|
||
wechatQrImg.className = "pa-wechat-qr-img";
|
||
|
||
// 加载成功清理超时定时器
|
||
wechatQrImg.onload = () => { if (qrFallbackTimer) { clearTimeout(qrFallbackTimer); qrFallbackTimer = null; } };
|
||
|
||
// 远程加载失败时回退到本地备用图(也带时间戳避免缓存)
|
||
wechatQrImg.onerror = () => {
|
||
if (qrFallbackTimer) { clearTimeout(qrFallbackTimer); qrFallbackTimer = null; }
|
||
if (wechatQrImg.dataset.fallbackApplied !== '1') {
|
||
loadLocalQr();
|
||
}
|
||
};
|
||
|
||
// 初次渲染和每次鼠标进入都触发重新加载
|
||
loadWechatQr();
|
||
wechatTag.addEventListener('mouseenter', loadWechatQr);
|
||
|
||
wechatQr.appendChild(wechatQrImg);
|
||
wechatTag.appendChild(wechatQr);
|
||
|
||
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(tUI("规则管理器"), async () => {
|
||
showRulesConfigModal();
|
||
}, false);
|
||
|
||
buttonCell.appendChild(button);
|
||
row.appendChild(buttonCell);
|
||
return row;
|
||
}
|
||
},
|
||
|
||
])
|
||
});
|
||
|
||
logger.log("小助手设置注册成功");
|
||
return true;
|
||
} catch (error) {
|
||
logger.error(`小助手设置注册失败: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|