Files
yawiii-ComfyUI-Prompt-Assis…/js/services/interceptor.js
T

1211 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 工作流自动翻译拦截器
* 在工作流执行时拦截CLIP节点的文本输入,实现自动翻译
*/
import { logger } from '../utils/logger.js';
import { PromptFormatter } from "../utils/promptFormatter.js";
import { TranslateCacheService } from "./cache.js";
import { APIService } from "./api.js";
import { FEATURES } from "./features.js";
// CLIP节点类型列表 - 导出以便其他模块使用
export const CLIP_NODE_TYPES = [
'CLIPTextEncode',
'CLIPTextEncodeSDXL',
'CLIPTextEncodeSD3',
'CLIPTextEncodeFlux',
'CLIPTextEncodeControlnet',
'CLIPTextEncodeHunyuanDiT',
'CLIPTextEncodeLumina2',
'CLIPTextEncodePixArtAlpha',
'CLIPTextEncodeSDXLRefiner'
];
// 可能输出文本的节点类型
const TEXT_OUTPUT_NODE_TYPES = [
'PrimitiveNode', // 原始节点
'Text', // 文本节点
'Note', // 笔记节点
'CLIPTextEncode', // CLIP文本编码(可能连接到其他CLIP节点)
'Reroute', // 路由节点,用于传递数据
'PromptText', // 提示词文本节点
'TextCombine', // 文本组合节点
'TextMultiline', // 多行文本节点
'StringFunction', // 字符串函数节点
'StringReplace', // 字符串替换节点
'StringVariable', // 字符串变量节点
'TextNode', // 通用文本节点类型
'TextInput', // 文本输入节点
'easy positive', // easy positive提示词节点
'easy negative', // easy negative提示词节点
'positive', // 普通提示词节点
'negative', // 普通负面提示词节点
'prompt', // 提示词节点
'Template', // 模板节点
'PromptTemplate' // 提示词模板节点
];
/**
* 检查节点是否为指定的CLIP类型
*/
function isClipNode(classType) {
return CLIP_NODE_TYPES.includes(classType);
}
/**
* 检查节点是否可能输出文本
* @param {string} classType - 节点类型
* @returns {boolean} - 如果节点可能输出文本,则返回true
*/
function isTextOutputNode(classType) {
// 精确匹配已知类型
if (TEXT_OUTPUT_NODE_TYPES.includes(classType)) {
return true;
}
// 使用名称匹配以扩展覆盖更多可能的文本节点
// 包含这些关键词的节点类型很可能是处理文本的
const textRelatedKeywords = [
'text', 'prompt', 'string', 'chat', 'word', 'message', 'note',
'positive', 'negative', 'template', 'gpt', 'llm', 'input'
];
return textRelatedKeywords.some(keyword =>
classType.toLowerCase().includes(keyword.toLowerCase()));
}
/**
* 获取节点的文本输入字段名
* 不同的CLIP节点可能有不同的文本输入字段
*/
function getTextInputFields(classType) {
switch (classType) {
case 'CLIPTextEncodeSDXL':
return ['text_g', 'text_l'];
case 'CLIPTextEncodeLumina2':
return ['system_prompt', 'user_prompt'];
case 'CLIPTextEncodePixArtAlpha':
case 'CLIPTextEncodeSDXLRefiner':
return ['text'];
default:
return ['text']; // 大多数CLIP节点使用'text'字段
}
}
/**
* 递归查找上游文本源
* @param {Object} apiWorkflow - API工作流
* @param {Array} link - 链接数组 [nodeId, outputSlot]
* @param {number} [depth=0] - 递归深度,防止无限循环
* @returns {Object|null} - 包含文本、源节点、源节点ID和源字段的对象,或null
*/
function findUpstreamTextSource(apiWorkflow, link, depth = 0) {
// 防止无限递归
if (!link || !Array.isArray(link) || link.length < 2 || depth > 10) {
logger.debug(`递归查找终止 | 链接无效或达到最大深度:${depth}`);
return null;
}
const [nodeId, outputSlot] = link;
const node = apiWorkflow[nodeId];
// 如果找不到节点,则停止
if (!node) {
logger.debug(`未找到节点 | ID:${nodeId}`);
return null;
}
logger.debug(`检查节点 | ID:${nodeId} | 类型:${node.class_type} | 输出槽:${outputSlot}`);
// 记录节点结构以便调试
const nodeInputsStr = JSON.stringify(node.inputs || {});
const nodeOutputsStr = JSON.stringify(node.outputs || {});
logger.debug(`节点详情 | 输入:${nodeInputsStr.substring(0, 200)} | 输出:${nodeOutputsStr.substring(0, 200)}`);
// ===== 通用数据提取 =====
// 1. 首先尝试从outputs数组中提取数据
if (node.outputs && Array.isArray(node.outputs) && node.outputs.length > outputSlot) {
const outputValue = node.outputs[outputSlot];
if (typeof outputValue === 'string') {
logger.debug(`从outputs数组中找到文本 | ID:${nodeId} | 槽:${outputSlot} | 文本:${outputValue.substring(0, 30)}`);
return {
text: outputValue,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: `outputs[${outputSlot}]`
};
} else if (outputValue && typeof outputValue === 'object') {
// 如果输出是对象,尝试提取其中的文本
const extractedText = extractTextFromValue(outputValue);
if (extractedText) {
logger.debug(`从outputs数组对象中提取文本 | ID:${nodeId} | 槽:${outputSlot} | 文本:${extractedText.substring(0, 30)}`);
return {
text: extractedText,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: `outputs[${outputSlot}]`,
isExtracted: true
};
}
}
}
// 2. 尝试从outputs对象中提取数据
if (node.outputs && typeof node.outputs === 'object' && !Array.isArray(node.outputs)) {
const possibleOutputKeys = ['text', 'value', 'string', 'output', 'result', `output_${outputSlot}`,
'positive', 'negative', 'prompt', 'content', 'message'];
for (const key of possibleOutputKeys) {
if (node.outputs[key] !== undefined) {
if (typeof node.outputs[key] === 'string') {
logger.debug(`从outputs对象找到文本 | ID:${nodeId} | 键:${key} | 文本:${node.outputs[key].substring(0, 30)}`);
return {
text: node.outputs[key],
sourceNode: node,
sourceNodeId: nodeId,
sourceField: `outputs.${key}`
};
} else if (node.outputs[key] && typeof node.outputs[key] === 'object') {
const extractedText = extractTextFromValue(node.outputs[key]);
if (extractedText) {
logger.debug(`从outputs对象属性中提取文本 | ID:${nodeId} | 键:${key} | 文本:${extractedText.substring(0, 30)}`);
return {
text: extractedText,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: `outputs.${key}`,
isExtracted: true
};
}
}
}
}
}
// 3. 尝试直接从节点属性提取数据
const nodeTextProps = ['output_value', 'value', 'text', 'string', 'content', 'message', 'prompt'];
for (const prop of nodeTextProps) {
if (node[prop] !== undefined && typeof node[prop] === 'string') {
logger.debug(`从节点属性中找到文本 | ID:${nodeId} | 属性:${prop} | 文本:${node[prop].substring(0, 30)}`);
return {
text: node[prop],
sourceNode: node,
sourceNodeId: nodeId,
sourceField: prop
};
} else if (node[prop] && typeof node[prop] === 'object') {
const extractedText = extractTextFromValue(node[prop]);
if (extractedText) {
logger.debug(`从节点属性对象中提取文本 | ID:${nodeId} | 属性:${prop} | 文本:${extractedText.substring(0, 30)}`);
return {
text: extractedText,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: prop,
isExtracted: true
};
}
}
}
// 4. 检查node.inputs中的所有字段,收集所有可能的文本字段
// 不再仅仅关注特定字段,而是检查所有可能包含文本的字段
if (node.inputs) {
// 先检查常见的文本字段名称
const commonTextFields = ['text', 'value', 'string', 'prompt', 'positive', 'negative',
'content', 'message', 'input', 'template'];
for (const field of commonTextFields) {
if (node.inputs[field] !== undefined) {
if (typeof node.inputs[field] === 'string') {
logger.debug(`在inputs的常见字段中找到文本 | ID:${nodeId} | 字段:${field} | 文本:${node.inputs[field].substring(0, 30)}`);
return {
text: node.inputs[field],
sourceNode: node,
sourceNodeId: nodeId,
sourceField: field
};
} else if (Array.isArray(node.inputs[field])) {
// 可能是链接到另一个节点
const upstreamLink = node.inputs[field];
logger.debug(`在字段${field}中找到上游链接 | ID:${nodeId} | 链接:${upstreamLink}`);
const result = findUpstreamTextSource(apiWorkflow, upstreamLink, depth + 1);
if (result) return result;
}
}
}
// 然后检查所有其他字段
logger.debug(`检查所有输入字段 | ID:${nodeId} | 可用字段:${Object.keys(node.inputs)}`);
// 收集可能的数组链接(递归来源)和字符串值
const possibleArrays = [];
const possibleStrings = [];
for (const [field, value] of Object.entries(node.inputs)) {
// 如果是字符串,收集起来稍后处理
if (typeof value === 'string') {
// 过滤掉太短或明显非文本内容的字段
if (value.trim().length > 2 && !/^[,.:;\-_+*/\\|]+$/.test(value)) {
possibleStrings.push({field, value});
}
}
// 如果是链接,收集起来稍后处理
else if (Array.isArray(value)) {
possibleArrays.push({field, value});
}
// 如果是对象,尝试提取文本
else if (value && typeof value === 'object') {
const extractedText = extractTextFromValue(value);
if (extractedText) {
logger.debug(`从输入对象中提取文本 | ID:${nodeId} | 字段:${field} | 文本:${extractedText.substring(0, 30)}`);
return {
text: extractedText,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: `inputs.${field}`,
isExtracted: true
};
}
}
}
// 首先处理可能的字符串,优先选择看起来像提示词的长字符串
if (possibleStrings.length > 0) {
// 按字符串长度排序,优先选择较长的字符串
possibleStrings.sort((a, b) => b.value.length - a.value.length);
for (const {field, value} of possibleStrings) {
// 如果看起来像提示词(包含空格且长度适中)或长度足够有意义,就返回
if (value.includes(' ') || value.length > 5) {
logger.debug(`选择可能的提示词文本 | ID:${nodeId} | 字段:${field} | 文本:${value.substring(0, 30)}`);
return {
text: value,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: field
};
}
}
// 如果没找到合适的,返回第一个
const {field, value} = possibleStrings[0];
logger.debug(`使用第一个可用文本 | ID:${nodeId} | 字段:${field} | 文本:${value}`);
return {
text: value,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: field
};
}
// 如果没找到字符串,再处理可能的数组链接
for (const {field, value} of possibleArrays) {
logger.debug(`尝试处理链接数组 | ID:${nodeId} | 字段:${field} | 链接:${value}`);
const result = findUpstreamTextSource(apiWorkflow, value, depth + 1);
if (result) return result;
}
}
// 5. 特殊处理:如果节点类型包含"concat"、"combine"、"join"等关键词
// 这些节点可能将多个输入文本组合在一起
if (node.class_type &&
(node.class_type.toLowerCase().includes('concat') ||
node.class_type.toLowerCase().includes('combine') ||
node.class_type.toLowerCase().includes('join'))) {
logger.debug(`检测到文本组合节点 | ID:${nodeId} | 类型:${node.class_type}`);
// 尝试收集所有可能的文本输入
const textParts = [];
// 检查inputs中的所有字段,寻找可能的文本输入或链接
if (node.inputs) {
for (const [field, value] of Object.entries(node.inputs)) {
// 跳过分隔符等非主要文本字段
if (field.toLowerCase().includes('separator') || field.toLowerCase().includes('delimiter')) {
continue;
}
// 如果是字符串,直接添加
if (typeof value === 'string' && value.trim().length > 0) {
textParts.push(value);
}
// 如果是链接,尝试获取上游文本
else if (Array.isArray(value)) {
const result = findUpstreamTextSource(apiWorkflow, value, depth + 1);
if (result && result.text) {
textParts.push(result.text);
}
}
}
}
// 如果找到了文本部分,组合它们
if (textParts.length > 0) {
const combinedText = textParts.join(' ');
logger.debug(`组合多个文本部分 | ID:${nodeId} | 部分数量:${textParts.length} | 组合结果:${combinedText.substring(0, 30)}...`);
return {
text: combinedText,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: 'combined_text',
isCombined: true
};
}
}
// 6. 如果找不到其他文本,尝试从节点名称或描述中提取信息
if (node.title && typeof node.title === 'string' && node.title.length > 3) {
logger.debug(`从节点标题中提取信息 | ID:${nodeId} | 标题:${node.title}`);
return {
text: node.title,
sourceNode: node,
sourceNodeId: nodeId,
sourceField: 'title'
};
}
// 7. 特殊处理:节点是路由节点(Reroute)
if (node.class_type === 'Reroute') {
logger.debug(`处理路由节点 | ID:${nodeId}`);
if (node.inputs && Object.values(node.inputs).length > 0) {
const upstreamLink = Object.values(node.inputs)[0];
// 确保上游连接是一个有效的链接数组
if (Array.isArray(upstreamLink)) {
logger.debug(`路由节点发现上游链接 | 链接:[${upstreamLink}]`);
return findUpstreamTextSource(apiWorkflow, upstreamLink, depth + 1);
}
}
}
logger.debug(`未能找到文本源 | ID:${nodeId} | 类型:${node.class_type}`);
return null;
}
/**
* 处理可能包含对象或复杂结构的文本值
* @param {any} value - 需要处理的值
* @returns {string|null} - 提取的文本或null
*/
function extractTextFromValue(value) {
// 如果是字符串,直接返回
if (typeof value === 'string') {
return value;
}
// 如果是对象,尝试提取text、value或其他常见文本字段
if (value && typeof value === 'object' && !Array.isArray(value)) {
// 常见的文本字段名
const textFields = ['text', 'value', 'content', 'string', 'prompt', 'message'];
for (const field of textFields) {
if (value[field] && typeof value[field] === 'string') {
return value[field];
}
}
// 如果有toString方法且不是默认的Object.toString
if (typeof value.toString === 'function' &&
value.toString !== Object.prototype.toString) {
const strValue = value.toString();
if (strValue !== '[object Object]') {
return strValue;
}
}
}
return null;
}
/**
* 检查文本是否需要翻译
* @param {any} text - 要检查的文本内容
* @returns {boolean} - 如果需要翻译返回true
*/
function shouldTranslateText(text) {
// 处理无效文本情况
if (text === undefined || text === null) {
logger.debug(`跳过翻译检查 | 原因:文本为空`);
return false;
}
// 如果不是字符串,尝试转换
if (typeof text !== 'string') {
// 尝试从复杂值中提取文本
const extractedText = extractTextFromValue(text);
if (!extractedText) {
logger.debug(`跳过翻译检查 | 原因:非文本类型 | 类型:${typeof text}`);
return false;
}
text = extractedText;
}
// 检查文本是否过短
if (text.trim().length < 3) {
logger.debug(`跳过翻译检查 | 原因:文本太短 | 文本:"${text}"`);
return false;
}
// 检查是否是特殊标记或控制字符
if (/^\s*\[(.*?)\]\s*$/.test(text) || /^<.*>$/.test(text)) {
logger.debug(`跳过翻译检查 | 原因:可能是特殊标记 | 文本:"${text}"`);
return false;
}
// 检查是否只包含非文本字符(如符号、数字等)
if (!/[\p{L}]/u.test(text)) {
logger.debug(`跳过翻译检查 | 原因:不含文字字符 | 文本:"${text}"`);
return false;
}
// 检测文本是否包含中文字符
// 使用正则表达式检查是否包含中文字符范围
const containsChinese = /[\u4e00-\u9fa5]/.test(text);
if (containsChinese) {
// 计算中文字符所占比例,判断是否值得翻译
const chineseChars = text.match(/[\u4e00-\u9fa5]/g) || [];
const chineseRatio = chineseChars.length / text.length;
// 如果中文字符比例大于1%,认为需要翻译
if (chineseRatio > 0.01) {
const textPreview = text.length > 50 ? `${text.substring(0, 50)}...` : text;
logger.debug(`检测到中文文本 | 中文字符:${chineseChars.length} | 中文比例:${(chineseRatio * 100).toFixed(1)}% | 文本预览:"${textPreview}"`);
return true;
}
}
// 如果上述快速检查未确定,则使用PromptFormatter的更完整语言检测
try {
const { from, to } = PromptFormatter.detectLanguage(text);
const needTranslate = from === 'zh';
// 记录详细的语言检测日志
const textPreview = text.length > 50 ? `${text.substring(0, 50)}...` : text;
logger.debug(`语言检测结果 | 源语言:${from} | 目标语言:${to} | 需要翻译:${needTranslate} | 文本预览:"${textPreview}"`);
return needTranslate;
} catch (error) {
logger.error(`语言检测异常 | 错误:${error.message} | 文本:"${text.substring(0, 30)}..."`);
// 发生异常时,回退到简单的中文检测
if (containsChinese) {
logger.debug(`语言检测异常,回退到简单中文检测,检测到中文字符`);
return true;
}
return false;
}
}
/**
* 生成请求ID
* @param {string|null} prefix - 前缀
* @param {string} type - 翻译类型
* @returns {string} - 请求ID
*/
function generateRequestId(prefix, type) {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
const typePrefix = type === "baidu" ? "BD" : "LLM";
return `${prefix || typePrefix}_${timestamp}_${random}`;
}
/**
* 翻译文本并使用缓存
*/
async function translateWithCache(text) {
try {
const app = window.app;
// 检查是否应该使用翻译缓存
const useCache = FEATURES?.useTranslateCache !== false; // 默认为true
// 如果启用了缓存,则查询翻译缓存
if (useCache) {
const cacheResult = TranslateCacheService.queryTranslateCache(text);
if (cacheResult && cacheResult.type === 'source') {
// 缓存命中,直接返回译文
logger.debug(`翻译缓存命中 | 文本:"${text}" | 译文:"${cacheResult.translatedText}"`);
return {
success: true,
translated: cacheResult.translatedText,
fromCache: true
};
}
} else {
logger.debug(`翻译缓存已禁用,将直接调用API | 文本:"${text}"`);
}
// 缓存未命中或未启用缓存,调用翻译API
const translateType = app.settings?.["PromptAssistant.Settings.TranslateType"] || "baidu";
// 生成唯一request_id,根据翻译类型生成对应的前缀
const request_id = generateRequestId(null, translateType);
// 检测语言
const langResult = PromptFormatter.detectLanguage(text);
let result;
if (translateType === "baidu") {
result = await APIService.baiduTranslate(text, langResult.from, langResult.to, null, true);
} else {
result = await APIService.llmTranslate(text, langResult.from, langResult.to, null, true);
}
if (result.success) {
// 格式化翻译结果
const formattedText = PromptFormatter.formatTranslatedText(result.data.translated);
// 如果启用了缓存,则添加到翻译缓存
if (useCache) {
TranslateCacheService.addTranslateCache(text, formattedText);
logger.debug(`添加翻译到缓存 | 文本:"${text}" | 译文:"${formattedText}"`);
}
return {
success: true,
translated: formattedText,
fromCache: false
};
} else {
throw new Error(result.error || '翻译失败');
}
} catch (error) {
return {
success: false,
error: error.message,
translated: text // 翻译失败时返回原文
};
}
}
/**
* 工作流自动翻译拦截器
*/
export class WorkflowAutoTranslateInterceptor {
/**
* 拦截并处理工作流数据
* @param {Object} workflowData - 包含output和workflow的工作流数据
* @returns {Promise<Object>} 处理后的工作流数据
*/
static async interceptWorkflow(workflowData) {
const { output: apiWorkflow, workflow } = workflowData;
let translationCount = 0;
let cacheHitCount = 0;
let skippedCount = 0;
let portTranslationCount = 0; // 输入端口翻译计数
let processedInputLinks = new Set(); // 用于跟踪已处理的输入链接,避免重复处理
// 存储所有翻译任务的Promise
const translationTasks = [];
// 存储CLIP节点信息,优先处理CLIP节点的输入
const clipNodes = [];
try {
logger.debug(`开始拦截工作流 | 节点数量:${Object.keys(apiWorkflow).length}`);
// 第一轮:先识别并收集所有CLIP节点
for (const [nodeId, nodeData] of Object.entries(apiWorkflow)) {
if (isClipNode(nodeData.class_type)) {
clipNodes.push({ nodeId, nodeData });
logger.debug(`发现CLIP节点 | ID:${nodeId} | 类型:${nodeData.class_type}`);
}
}
logger.debug(`找到CLIP节点总数:${clipNodes.length}`);
// 第二轮:处理所有CLIP节点
for (const { nodeId, nodeData } of clipNodes) {
logger.debug(`处理CLIP节点 | ID:${nodeId} | 类型:${nodeData.class_type}`);
// 获取该节点类型的文本输入字段
const textFields = getTextInputFields(nodeData.class_type);
// 处理每个文本字段
for (const fieldName of textFields) {
const input = nodeData.inputs[fieldName];
if (!input) {
continue;
}
// 处理直接输入的文本(字符串形式)
if (typeof input === 'string') {
if (shouldTranslateText(input)) {
logger.debug(`发现直接输入文本 | 节点:${nodeId} | 字段:${fieldName} | 文本:"${input.substring(0, 30)}..."`);
// 创建翻译任务并添加到任务列表
const translationTask = (async () => {
try {
const translateResult = await translateWithCache(input);
if (translateResult.success) {
// 替换API工作流中的文本(传给后端的数据)
nodeData.inputs[fieldName] = translateResult.translated;
translationCount++;
if (translateResult.fromCache) {
cacheHitCount++;
}
logger.debug(`直接输入文本已翻译 | 节点:${nodeId} | 字段:${fieldName} | 原文:"${input}" | 译文:"${translateResult.translated}"`);
}
} catch (error) {
logger.error(`直接输入文本翻译失败 | 节点:${nodeId} | 字段:${fieldName} | 错误:${error.message}`);
// 翻译失败时保留原文
nodeData.inputs[fieldName] = input;
}
})();
translationTasks.push(translationTask);
} else {
skippedCount++;
}
}
// 处理输入端口连接的情况(数组形式 [nodeId, outputSlot])
else if (Array.isArray(input) && input.length >= 2) {
// 创建唯一的链接ID以避免重复处理
const linkId = `${input[0]}_${input[1]}_${nodeId}_${fieldName}`;
// 如果这个链接已经处理过,则跳过
if (processedInputLinks.has(linkId)) {
logger.debug(`跳过重复链接 | 链接ID:${linkId}`);
continue;
}
processedInputLinks.add(linkId);
// 记录链接信息
logger.debug(`处理输入端口连接 | 节点:${nodeId} | 字段:${fieldName} | 输入链接:[${input[0]},${input[1]}]`);
// ===== 直接从输入源提取整合后的完整文本,而不是从上游节点翻译 =====
const sourceNodeId = input[0];
const sourceOutputSlot = input[1];
const sourceNode = apiWorkflow[sourceNodeId];
// 判断源节点是否是文本组合类型节点
const isTextCombineNode = sourceNode && sourceNode.class_type && (
sourceNode.class_type.toLowerCase().includes('concat') ||
sourceNode.class_type.toLowerCase().includes('combine') ||
sourceNode.class_type.toLowerCase().includes('join')
);
// 如果是文本组合节点,尝试获取其完整输出
let combinedText = null;
if (isTextCombineNode) {
logger.debug(`检测到文本组合节点 | ID:${sourceNodeId} | 类型:${sourceNode.class_type}`);
// 尝试从组合节点直接获取完整输出
if (sourceNode.outputs && Array.isArray(sourceNode.outputs) && sourceNode.outputs.length > sourceOutputSlot) {
if (typeof sourceNode.outputs[sourceOutputSlot] === 'string') {
combinedText = sourceNode.outputs[sourceOutputSlot];
logger.debug(`获取到组合节点输出 | 文本:"${combinedText.substring(0, 30)}..."`);
}
}
// 如果没有找到直接输出,尝试组合所有输入
if (!combinedText) {
const textParts = [];
let separator = ' '; // 默认分隔符
// 尝试查找分隔符
if (sourceNode.inputs && sourceNode.inputs.separator) {
if (typeof sourceNode.inputs.separator === 'string') {
separator = sourceNode.inputs.separator;
logger.debug(`使用自定义分隔符: "${separator}"`);
}
}
// 收集所有输入
if (sourceNode.inputs) {
for (const [key, value] of Object.entries(sourceNode.inputs)) {
// 跳过分隔符字段
if (key.toLowerCase().includes('separator') || key.toLowerCase().includes('delimiter')) {
continue;
}
// 直接字符串输入
if (typeof value === 'string') {
textParts.push(value);
logger.debug(`添加文本部分: "${value.substring(0, 20)}..."`);
}
// 链接输入
else if (Array.isArray(value)) {
const upstreamText = findUpstreamTextSource(apiWorkflow, value);
if (upstreamText && typeof upstreamText.text === 'string') {
textParts.push(upstreamText.text);
logger.debug(`添加上游文本: "${upstreamText.text.substring(0, 20)}..."`);
}
}
}
}
// 组合所有文本部分
if (textParts.length > 0) {
combinedText = textParts.join(separator);
logger.debug(`组合多个部分得到文本 | 部分数:${textParts.length} | 结果:"${combinedText.substring(0, 30)}..."`);
}
}
}
// 如果找到了组合文本且需要翻译
if (combinedText && shouldTranslateText(combinedText)) {
logger.debug(`对组合节点的完整文本进行翻译 | 文本:"${combinedText.substring(0, 30)}..."`);
const translationTask = (async () => {
try {
const translateResult = await translateWithCache(combinedText);
if (translateResult.success) {
// 创建一个特殊字段保存完整的翻译结果,以便后续替换
nodeData._translatedInput = {
field: fieldName,
originalLink: input.slice(), // 复制链接数组
translatedText: translateResult.translated
};
// 修改输入为直接文本
nodeData.inputs[fieldName] = translateResult.translated;
translationCount++;
portTranslationCount++;
if (translateResult.fromCache) {
cacheHitCount++;
}
logger.debug(`组合文本已翻译 | 节点:${nodeId} | 字段:${fieldName} | 原文:"${combinedText}" | 译文:"${translateResult.translated}"`);
return; // 成功翻译组合文本,不再继续查找上游文本源
}
} catch (error) {
logger.error(`组合文本翻译失败 | 节点:${nodeId} | 字段:${fieldName} | 错误:${error.message}`);
}
})();
translationTasks.push(translationTask);
continue; // 跳过后续处理
}
// 如果没有找到组合文本或翻译失败,则继续使用常规方法递归查找上游文本源
const textSourceInfo = findUpstreamTextSource(apiWorkflow, input);
// 如果找到文本源
if (textSourceInfo) {
// 尝试从文本源中提取文本
let extractedText = textSourceInfo.text;
// 如果text不是字符串,尝试提取
if (extractedText !== undefined && typeof extractedText !== 'string') {
const extractedValue = extractTextFromValue(extractedText);
if (extractedValue) {
// 更新textSourceInfo中的文本
extractedText = extractedValue;
textSourceInfo.text = extractedValue;
logger.debug(`成功从复杂数据结构中提取文本 | 节点:${nodeId} | 文本:"${extractedValue.substring(0, 30)}..."`);
}
}
// 如果有文本并且需要翻译
if (extractedText && typeof extractedText === 'string' && shouldTranslateText(extractedText)) {
const { sourceNode, sourceNodeId, sourceField } = textSourceInfo;
logger.debug(`发现上游文本源 | 节点:${nodeId} | 字段:${fieldName} | 源节点:${sourceNodeId} | 源字段:${sourceField} | 文本:"${extractedText.substring(0, 30)}..."`);
// 创建翻译任务
const translationTask = (async () => {
try {
const translateResult = await translateWithCache(extractedText);
if (translateResult.success) {
// 直接更新源节点的文本值
if (sourceField.startsWith('outputs[') && sourceField.endsWith(']')) {
// 处理outputs数组情况
const outputIndex = parseInt(sourceField.match(/\[(\d+)\]/)[1], 10);
if (Array.isArray(sourceNode.outputs)) {
sourceNode.outputs[outputIndex] = translateResult.translated;
}
} else if (sourceField.includes('.')) {
// 处理嵌套属性情况
const parts = sourceField.split('.');
let target = sourceNode;
for (let i = 0; i < parts.length - 1; i++) {
target = target[parts[i]];
}
target[parts[parts.length - 1]] = translateResult.translated;
} else {
// 标准情况
if (sourceNode.inputs && sourceField in sourceNode.inputs) {
sourceNode.inputs[sourceField] = translateResult.translated;
} else {
sourceNode[sourceField] = translateResult.translated;
}
}
portTranslationCount++;
translationCount++;
if (translateResult.fromCache) {
cacheHitCount++;
}
logger.debug(`输入端口文本已翻译 | 节点:${nodeId} | 源节点:${sourceNodeId} | 源字段:${sourceField} | 原文:"${extractedText.substring(0, 30)}..." | 译文:"${translateResult.translated.substring(0, 30)}..."`);
} else {
logger.warn(`输入端口文本翻译失败 | 节点:${nodeId} | 源节点:${sourceNodeId} | 源字段:${sourceField} | 原因:${translateResult.error}`);
}
} catch (error) {
logger.error(`输入端口文本翻译异常 | 节点:${nodeId} | 源节点:${sourceNodeId} | 错误:${error.message}`);
// 翻译失败时保留原文
}
})();
translationTasks.push(translationTask);
} else {
if (!extractedText) {
logger.debug(`上游文本源无有效文本 | 节点:${nodeId} | 字段:${fieldName} | 源节点:${textSourceInfo.sourceNodeId} | 源字段:${textSourceInfo.sourceField} | 值类型:${typeof textSourceInfo.text}`);
} else if (typeof extractedText !== 'string') {
logger.debug(`上游文本不是字符串 | 节点:${nodeId} | 字段:${fieldName} | 源节点:${textSourceInfo.sourceNodeId} | 值类型:${typeof extractedText}`);
} else {
logger.debug(`跳过翻译 | 节点:${nodeId} | 字段:${fieldName} | 源节点:${textSourceInfo.sourceNodeId} | 原因:不需要翻译`);
}
skippedCount++;
}
} else {
logger.debug(`未找到上游文本源 | 节点:${nodeId} | 字段:${fieldName} | 输入链接:[${input[0]},${input[1]}]`);
// 尝试直接访问源节点
try {
const sourceNodeId = input[0];
const sourceOutputSlot = input[1];
const sourceNode = apiWorkflow[sourceNodeId];
if (sourceNode) {
logger.debug(`尝试直接访问源节点 | 源节点ID:${sourceNodeId} | 类型:${sourceNode.class_type}`);
// 创建一个新链接,使用通用的findUpstreamTextSource函数
const textSourceInfo = findUpstreamTextSource(apiWorkflow, [sourceNodeId, sourceOutputSlot], 0);
if (textSourceInfo && textSourceInfo.text) {
logger.debug(`直接访问源节点成功找到文本 | 源节点:${sourceNodeId} | 文本:"${textSourceInfo.text.substring(0, 30)}..."`);
// 如果找到了文本源且需要翻译,创建翻译任务
if (shouldTranslateText(textSourceInfo.text)) {
const translationTask = (async () => {
try {
const translateResult = await translateWithCache(textSourceInfo.text);
if (translateResult.success) {
// 更新源节点的文本
// 根据返回的sourceField确定如何更新
const { sourceNode, sourceField, isExtracted, isCombined } = textSourceInfo;
// 根据sourceField类型和位置更新源节点
if (sourceField === 'combined_text' || isExtracted || isCombined) {
// 特殊情况:组合或提取的文本
logger.debug(`翻译组合/提取文本 | 源节点:${sourceNodeId} | 字段:${sourceField}`);
}
else if (sourceField.startsWith('outputs[') && sourceField.endsWith(']')) {
// 处理outputs数组情况
const match = sourceField.match(/\[(\d+)\]/);
if (match) {
const outputIndex = parseInt(match[1], 10);
if (Array.isArray(sourceNode.outputs)) {
sourceNode.outputs[outputIndex] = translateResult.translated;
logger.debug(`更新源节点outputs数组 | 源节点:${sourceNodeId} | 索引:${outputIndex}`);
}
}
}
else if (sourceField.startsWith('outputs.')) {
// 处理outputs对象属性
const key = sourceField.substring('outputs.'.length);
if (sourceNode.outputs && typeof sourceNode.outputs === 'object') {
sourceNode.outputs[key] = translateResult.translated;
logger.debug(`更新源节点outputs对象 | 源节点:${sourceNodeId} | 键:${key}`);
}
}
else if (sourceField.startsWith('inputs.')) {
// 处理inputs对象属性
const key = sourceField.substring('inputs.'.length);
if (sourceNode.inputs && typeof sourceNode.inputs === 'object') {
sourceNode.inputs[key] = translateResult.translated;
logger.debug(`更新源节点inputs对象 | 源节点:${sourceNodeId} | 键:${key}`);
}
}
else {
// 直接字段或属性
if (sourceNode.inputs && sourceField in sourceNode.inputs) {
// 更新inputs中的字段
sourceNode.inputs[sourceField] = translateResult.translated;
logger.debug(`更新源节点inputs字段 | 源节点:${sourceNodeId} | 字段:${sourceField}`);
} else {
// 更新节点直接属性
sourceNode[sourceField] = translateResult.translated;
logger.debug(`更新源节点属性 | 源节点:${sourceNodeId} | 属性:${sourceField}`);
}
}
// 通用回退:如果无法确定准确的位置,尝试在所有位置寻找
let needFallback = true;
// 检查更新是否成功
if (sourceField === 'combined_text' || isExtracted || isCombined) {
// 对于组合文本或提取文本,需要逐个检查所有位置
needFallback = true;
}
else if (sourceField.startsWith('outputs[')) {
const match = sourceField.match(/\[(\d+)\]/);
if (match) {
const outputIndex = parseInt(match[1], 10);
if (Array.isArray(sourceNode.outputs) &&
sourceNode.outputs[outputIndex] === translateResult.translated) {
needFallback = false;
}
}
}
else if (sourceField.startsWith('outputs.')) {
const key = sourceField.substring('outputs.'.length);
if (sourceNode.outputs && sourceNode.outputs[key] === translateResult.translated) {
needFallback = false;
}
}
else if (sourceField.startsWith('inputs.')) {
const key = sourceField.substring('inputs.'.length);
if (sourceNode.inputs && sourceNode.inputs[key] === translateResult.translated) {
needFallback = false;
}
}
else {
if (sourceNode.inputs && sourceField in sourceNode.inputs &&
sourceNode.inputs[sourceField] === translateResult.translated) {
needFallback = false;
} else if (sourceNode[sourceField] === translateResult.translated) {
needFallback = false;
}
}
// 如果需要回退,则尝试所有位置
if (needFallback) {
logger.debug(`尝试回退方式更新源节点 | 源节点:${sourceNodeId}`);
// 1. 检查inputs中的所有字段
if (sourceNode.inputs) {
for (const [key, value] of Object.entries(sourceNode.inputs)) {
if (value === textSourceInfo.text) {
sourceNode.inputs[key] = translateResult.translated;
logger.debug(`回退更新: inputs.${key} | 源节点:${sourceNodeId}`);
needFallback = false;
break;
}
}
}
// 2. 检查outputs数组
if (needFallback && Array.isArray(sourceNode.outputs)) {
for (let i = 0; i < sourceNode.outputs.length; i++) {
if (sourceNode.outputs[i] === textSourceInfo.text) {
sourceNode.outputs[i] = translateResult.translated;
logger.debug(`回退更新: outputs[${i}] | 源节点:${sourceNodeId}`);
needFallback = false;
break;
}
}
}
// 3. 检查outputs对象
if (needFallback && sourceNode.outputs && typeof sourceNode.outputs === 'object') {
for (const [key, value] of Object.entries(sourceNode.outputs)) {
if (value === textSourceInfo.text) {
sourceNode.outputs[key] = translateResult.translated;
logger.debug(`回退更新: outputs.${key} | 源节点:${sourceNodeId}`);
needFallback = false;
break;
}
}
}
// 4. 检查节点属性
if (needFallback) {
for (const [key, value] of Object.entries(sourceNode)) {
if (value === textSourceInfo.text && typeof value === 'string') {
sourceNode[key] = translateResult.translated;
logger.debug(`回退更新: ${key} | 源节点:${sourceNodeId}`);
needFallback = false;
break;
}
}
}
}
portTranslationCount++;
translationCount++;
if (translateResult.fromCache) {
cacheHitCount++;
}
logger.debug(`输入端口文本已翻译 | 源节点:${sourceNodeId} | 原文:"${textSourceInfo.text.substring(0, 30)}..." | 译文:"${translateResult.translated.substring(0, 30)}..."`);
} else {
logger.warn(`输入端口文本翻译失败 | 源节点:${sourceNodeId} | 原因:${translateResult.error}`);
}
} catch (error) {
logger.error(`输入端口文本翻译异常 | 源节点:${sourceNodeId} | 错误:${error.message}`);
}
})();
translationTasks.push(translationTask);
} else {
logger.debug(`跳过翻译 | 源节点:${sourceNodeId} | 原因:不需要翻译`);
}
} else {
logger.debug(`直接访问源节点未找到文本 | 源节点:${sourceNodeId}`);
}
}
} catch (error) {
logger.error(`直接访问源节点异常 | 错误:${error.message} | 堆栈:${error.stack}`);
}
skippedCount++;
}
}
}
}
// 等待所有翻译任务完成
if (translationTasks.length > 0) {
logger.debug(`等待${translationTasks.length}个翻译任务完成...`);
await Promise.all(translationTasks);
}
if (translationCount > 0) {
logger.log(`工作流翻译完成 | 总计:${translationCount} | 缓存命中:${cacheHitCount} | 跳过:${skippedCount} | 输入端口:${portTranslationCount}`);
} else {
logger.debug(`工作流无需翻译 | 跳过:${skippedCount}`);
}
} catch (error) {
logger.error(`工作流翻译拦截异常 | 错误:${error.message}`);
}
return workflowData;
}
}
/**
* 安装自动翻译拦截器(一次性)
* 采用统一的功能开关逻辑,与其他功能保持一致
* @param {number} [maxRetries=10] - 最大重试次数
* @param {number} [retryDelay=500] - 重试延迟(毫秒)
* @returns {Promise<boolean>} - 安装是否成功
*/
export function ensureAutoTranslateInterceptorInstalled(maxRetries = 10, retryDelay = 1000) { // 重试间隔从500ms增加到1000ms
let currentRetry = 0;
const attemptInstall = () => {
try {
const app = window.app;
// 检查是否已经安装过拦截器
if (app && app.api && app.api._autoTranslateInstalled) {
logger.debug('自动翻译拦截器已安装,无需重复安装');
return true;
}
// 如果API还没准备好,稍后再试
if (!app || !app.api || typeof app.api.queuePrompt !== 'function') {
if (currentRetry < maxRetries) {
currentRetry++;
logger.debug(`API未准备好,${retryDelay}ms后重试(${currentRetry}/${maxRetries})...`);
setTimeout(() => attemptInstall(), retryDelay);
return false;
} else {
logger.error(`安装自动翻译拦截器失败: 达到最大重试次数(${maxRetries}),API未准备好`);
return false;
}
}
// 保存原始方法
app.api._originalQueuePrompt = app.api.queuePrompt.bind(app.api);
app.api._autoTranslateInstalled = true;
// 重写queuePrompt方法
app.api.queuePrompt = async function (number, data) {
try {
// 检查自动翻译功能是否启用(使用统一的FEATURES对象)
let features = { autoTranslate: true }; // 默认启用
try {
// 动态导入功能开关配置
const { FEATURES } = await import('./features.js');
features = FEATURES;
} catch (importError) {
logger.warn(`导入功能开关模块失败,使用默认设置 | 错误:${importError.message}`);
}
// 如果自动翻译功能已启用,则拦截并处理工作流数据
if (features.autoTranslate) {
// 记录调试信息
logger.debug(`拦截工作流请求 | 编号:${number}`);
// 在发送前拦截并处理工作流数据
const processedData = await WorkflowAutoTranslateInterceptor.interceptWorkflow(data);
// 调用原始方法,传递处理后的数据
return await app.api._originalQueuePrompt(number, processedData);
} else {
logger.debug('自动翻译功能已禁用,跳过处理');
// 功能关闭时直接调用原始方法
return await app.api._originalQueuePrompt(number, data);
}
} catch (error) {
// 出现异常时记录错误并继续使用原始数据
logger.error(`工作流拦截处理失败 | 错误:${error.message}`);
logger.error(`错误堆栈: ${error.stack}`);
// 发生错误时使用原始数据,确保即使翻译失败,原始功能仍然可用
return await app.api._originalQueuePrompt(number, data);
}
};
// 添加卸载方法,便于日后调试或清理
app.api._uninstallAutoTranslate = function() {
if (app.api._originalQueuePrompt) {
app.api.queuePrompt = app.api._originalQueuePrompt;
delete app.api._originalQueuePrompt;
delete app.api._autoTranslateInstalled;
logger.log('自动翻译拦截器已卸载');
return true;
}
return false;
};
// 添加重新安装方法
app.api._reinstallAutoTranslate = function() {
if (app.api._uninstallAutoTranslate()) {
return ensureAutoTranslateInterceptorInstalled();
}
return false;
};
logger.log('自动翻译拦截器已成功安装(统一功能开关模式)');
return true;
} catch (error) {
logger.error(`自动翻译拦截器安装异常 | 错误:${error.message}`);
logger.error(`错误堆栈: ${error.stack}`);
// 尝试错误恢复
if (currentRetry < maxRetries) {
currentRetry++;
logger.debug(`安装失败,${retryDelay * 2}ms后重试(${currentRetry}/${maxRetries})...`);
setTimeout(() => attemptInstall(), retryDelay * 2);
return false;
}
return false;
}
};
// 开始安装尝试
return attemptInstall();
}
// 移除自动安装逻辑,改为由index.js直接控制初始化
// 这样可以避免与PromptAssistant.js的相互依赖,并使初始化流程更清晰