UNPKG

koishi-plugin-comfyui-kontext

Version:

支持图生图与图生视频的 Koishi 插件,基于 ComfyUI + DeepSeek

811 lines (713 loc) 43 kB
// lib/index.js // 引入依赖 const { Context, Schema, Logger, h } = require('koishi'); const { v4: uuidv4 } = require('uuid'); const FormData = require('form-data'); const axios = require('axios'); const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); // 日志实例 const logger = new Logger('comfyui-img-video'); exports.name = 'comfyui-img-video-file'; // 图生图工作流配置 Schema const ImageWorkflowConfig = Schema.object({ alias: Schema.string().description('工作流的别名,用于在指令中调用。').required(), is_default: Schema.boolean().description('是否为默认工作流?(只能有一个默认)').default(false), permission_level: Schema.number().description('使用此工作流所需的权限等级。0 为所有人可用。').default(0), file_path: Schema.string().description('此工作流的本地文件路径(API JSON格式)。').required(), load_image_node_ids: Schema.union([ Schema.array(Schema.string()).description('多个图片输入节点ID'), Schema.string().description('单图片输入节点ID') ]).description('此工作流中用于加载输入图片的节点ID(支持多个)').required(), positive_prompt_node_id: Schema.string().description('此工作流中填写【正面提示词】的节点的ID。').required(), default_positive_prompt: Schema.string().description('默认正向提示词。当用户不提供任何提示词时,将使用此内容(且不经过AI处理)。').default('') }).description('图生图工作流配置'); // 图生视频工作流配置 Schema const VideoWorkflowConfig = Schema.object({ alias: Schema.string().description('工作流的别名,用于在指令中调用。').required(), is_default: Schema.boolean().description('是否为默认工作流?(只能有一个默认)').default(false), permission_level: Schema.number().description('使用此工作流所需的权限等级。0 为所有人可用。').default(0), file_path: Schema.string().description('此工作流的本地文件路径(API JSON格式)。').required(), load_image_node_id: Schema.string().description('此工作流中用于加载【单张】输入图片的节点ID。').required(), positive_prompt_node_id: Schema.string().description('此工作流中填写【正面提示词】的节点的ID。').required(), default_positive_prompt: Schema.string().description('默认正向提示词。当用户不提供任何提示词时,将使用此内容(且不经过AI处理)。').default(''), }).description('图生视频工作流配置'); // 插件配置 Schema exports.Config = Schema.object({ comfyui: Schema.object({ server_address: Schema.string().description('ComfyUI 服务器地址').default('127.0.0.1:8188'), request_timeout: Schema.number().description('【图片】生成请求超时时间(秒)').default(120), video_request_timeout: Schema.number().description('【视频】生成请求超时时间(秒),通常需要更长时间。').default(300), }).description('ComfyUI 设置'), workflows: Schema.array(ImageWorkflowConfig).description('【图生图】工作流配置列表'), video_workflows: Schema.array(VideoWorkflowConfig).description('【图生视频】工作流配置列表 (仅支持单图输入)'), prompt_engineer: Schema.object({ enable: Schema.boolean().description('是否启用 DeepSeek 功能。').default(false), deepseek_api_key: Schema.string().description('您的 DeepSeek API 令牌。').default('').role('secret'), }).description('DeepSeek 功能设置 (用于提示词工程与翻译)'), }); // 你写死的"转手办"英文提示词 const HANDMADE_PROMPT = `Transform the image to a detailed anime figure model style with high-quality finish and realistic textures, while keeping the character design and original composition completely unchanged.`; // [NEW] 新增的 FusionX 视频专用提示词 const FUSIONX_VIDEO_PROMPT = ` 🔧 FusionX 提示词核心公式 [主体] + [动作/状态] + [场景/环境] + [镜头语言] + [风格化描述] + [细节增强指令] (通过精准描述触发 FusionX 的动态渲染、风格融合与超分辨率能力) 1. 主体与动作的动态控制 模板: [主体] [动词+补语], [物理特性描述], [运动轨迹/节奏], [情感表达] 示例: 基础:A robot walks forward, metallic legs bending with each step FusionX 增强: A biomechanical dragon soars through storm clouds, scales shimmering with electric blue energy, wings beating in rhythmic arcs, head tilted with curiosity as lightning crackles nearby 细节增强:hydraulic joints hissing with each movement, raindrops splattering off its armor 2. 场景与氛围的沉浸式构建 模板: [时间/天气] [场景], [光影效果], [氛围关键词], [空间层次], [感官细节] 示例: 基础:A futuristic city at night, neon lights reflecting on wet streets FusionX 增强: Dawn breaks over a cyberpunk metropolis, holographic billboards cast cyan glow on rain-slicked alleys, dense fog swirls around skyscrapers, distant sirens wail as drones zip past, the air smells of ozone and fried noodles 细节增强:water puddles ripple with each footstep, neon signs flicker with static interference 3. 风格化与艺术融合 模板: In the style of [艺术流派/影视参考], [材质/笔触描述], [色彩基调], [构图规则], [创新元素] 示例: 基础:In the style of Studio Ghibli, hand-painted textures FusionX 增强: In the style of Hayao Miyazaki meets cyberpunk, watercolor backgrounds with ink outlines, vibrant orange sunset contrasting neon grid, third-person cinematic framing, add floating holographic koi fish swimming through the air 细节增强:brush strokes visible in the clouds, ink bleed effects on the neon signs 4. 镜头语言与运动指令 模板: [镜头类型] [运动方式], [焦距/景深], [拍摄速度], [视角描述], [动态效果] 示例: 基础:Close-up shot, slow motion FusionX 增强: Dutch angle tracking shot, 35mm lens with shallow depth of field, 120fps slow motion capturing water droplets suspended in mid-air, from behind the protagonist's shoulder, add lens flare and chromatic aberration 细节增强:background objects blur into abstract color fields, motion lines trail the subject 5. FusionX 专属参数联动技巧 运动幅度控制: [主体] performs [动作] with [强度描述+物理反馈] 示例:a samurai swings his katana with explosive force, air shrieks as the blade cuts through mist 细节层级指定: [物体] features [细节类型] at [精度级别+动态表现] 示例:the dragon's scales have intricate patterns that shift color with each wingbeat, 8K resolution with micro-surface reflections 时间轴控制: [场景] transitions from [初始状态] to [最终状态] over [时长+动态逻辑] 示例:the sky shifts from sunset to starry night over 5 seconds, stars twinkle into existence as city lights dim 📌 实战案例:生成一段科幻短片提示词 目标:机械章鱼在海底火山喷发中逃生 FusionX 提示词: A biomechanical octopus with glowing blue circuits flees an underwater volcanic eruption, metallic tentacles thrash with hydraulic precision, lava bubbles rise in slow-motion explosions, deep-sea pressure distorts light rays into kaleidoscopic patterns, wide-angle shot from below the seabed, in the style of concept art by Syd Mead with iridescent bioluminescence, 8K resolution --Add dynamic effect: "heat haze warping the water near lava flows" ⚡ 效率提升技巧 模块化组合: 将常用描述拆解为可复用模块(如 [LIGHTING] = "cyan neon glow with orange contrast"),快速拼接提示词。 参数缩写: 4K, 24fps, HDR10+ 可简写为 Cinematic 4K HDR,slow motion 替换为 120fps bullet time effect。 感官扩展: 增加非视觉描述(如 the engine roars with deep bass frequencies)激发模型的多模态渲染能力。 输出示例: 用户需求:生成一段赛博朋克风格的角色介绍视频 FusionX 提示词: A cybernetically enhanced female bounty hunter stands in a rain-soaked alley, neon signs casting cyan highlights on her leather trench coat, holographic ads flicker overhead with Japanese kanji, first-person POV shot with motion blur, in the style of Blade Runner 2049 with gritty film grain, 4K 60fps --Add detail: "raindrops sizzle as they hit her heated cybernetic arm" 你需要根据用户输入的需求,遵循以上规则,直接输出一段完整的、高质量的英文提示词。不要解释,不要分段,不要添加任何额外备注。 `; exports.apply = (ctx, config) => { const request_queue = []; // 处理队列 let is_processing = false; // 是否有任务正在处理 const max_queue_size = 5; // 最大队列长度 const pendingImageSessions = new Map(); function getSessionKey(session) { return `${session.userId}#${session.channelId}`; } // 主指令:img2img/改图 ctx.command('img2img <prompt:text>', '使用ComfyUI进行图生图') .alias('改图') .option('raw', '-r 直接使用原始提示词,不经过AI处理') .option('translateOnly', '-t 仅将提示词简单翻译为英文后,再生成图片') .action(async ({ session, options }, prompt_text) => { const full_prompt = prompt_text || ''; const words = full_prompt.split(' ').filter(word => word); const first_word = words[0] || ''; let final_alias = null; let final_prompt = full_prompt; const matched_workflow = config.workflows.find(w => w.alias === first_word); if (matched_workflow) { final_alias = first_word; final_prompt = words.slice(1).join(' '); } let target_workflow_config; if (final_alias) { target_workflow_config = config.workflows.find(w => w.alias === final_alias); } else { target_workflow_config = config.workflows.find(w => w.is_default) || config.workflows[0]; } if (!target_workflow_config) { return '错误:插件未配置任何【图生图】工作流,无法处理请求。'; } if (target_workflow_config.permission_level > session.user.authority) { return `您的权限不足 (level ${session.user.authority}),无法使用此工作流 (需要 level ${target_workflow_config.permission_level})。`; } let imageNodeIds = target_workflow_config.load_image_node_ids; if (!Array.isArray(imageNodeIds)) imageNodeIds = [imageNodeIds]; const needCount = imageNodeIds.length; let imageElements = []; if (session.elements) imageElements = imageElements.concat(session.elements.filter(e => e.type === 'image' || e.type === 'img')); if (session.quote && session.quote.elements) imageElements = imageElements.concat(session.quote.elements.filter(e => e.type === 'image' || e.type === 'img')); const queue_item = { type: 'image', session, target_workflow_config, prompt_text: final_prompt, use_raw_prompt: options.raw || (final_prompt.toLowerCase() === 'remove clothes'), use_simple_translate: options.translateOnly, }; if (imageElements.length >= needCount) { request_queue.push({ ...queue_item, imageElements: imageElements.slice(0, needCount) }); await session.send(`你的任务已加入队列,当前排在第 ${request_queue.length} 位。`); logger.info(`新[图片]请求加入队列。当前队列长度: ${request_queue.length}`); process_queue(); return; } const key = getSessionKey(session); pendingImageSessions.set(key, { ...queue_item, images: [], needCount }); await session.send(`请发送第1张图片(共${needCount}张)。`); }); // 转手办指令 ctx.command('转手办', '将图片转为手办风格') .action(async ({ session }) => { let target_workflow_config = config.workflows.find(w => w.is_default) || config.workflows[0]; if (!target_workflow_config) return '错误:插件未配置任何【默认】图生图工作流,无法处理请求。'; if (target_workflow_config.permission_level > session.user.authority) return `您的权限不足 (level ${session.user.authority}),无法使用此工作流 (需要 level ${target_workflow_config.permission_level})。`; let imageNodeIds = target_workflow_config.load_image_node_ids; if (!Array.isArray(imageNodeIds)) imageNodeIds = [imageNodeIds]; const needCount = imageNodeIds.length; let imageElements = []; if (session.elements) imageElements = imageElements.concat(session.elements.filter(e => e.type === 'image' || e.type === 'img')); if (session.quote && session.quote.elements) imageElements = imageElements.concat(session.quote.elements.filter(e => e.type === 'image' || e.type === 'img')); const queue_item = { type: 'image', session, target_workflow_config, prompt_text: HANDMADE_PROMPT, use_raw_prompt: true, use_simple_translate: false, }; if (imageElements.length >= needCount) { request_queue.push({ ...queue_item, imageElements: imageElements.slice(0, needCount) }); await session.send(`你的任务已加入队列,当前排在第 ${request_queue.length} 位。`); logger.info(`新[手办]请求加入队列。当前队列长度: ${request_queue.length}`); process_queue(); return; } const key = getSessionKey(session); pendingImageSessions.set(key, { ...queue_item, images: [], needCount }); await session.send(`请发送第1张图片(共${needCount}张)。`); }); // 转视频指令 ctx.command('转视频 <prompt:text>', '将图片转为视频,可选子命令 "横" 或 "竖"') .alias('i2v') .option('raw', '-r 直接使用原始提示词,不经过AI处理') .option('translateOnly', '-t 仅将提示词简单翻译为英文后,再生成视频') .action(async ({ session, options }, prompt_text) => { let orientation = 'vertical'; let final_alias = null; const words = (prompt_text || '').split(' ').filter(Boolean); let prompt_words = [...words]; if (prompt_words[0] === '横') { orientation = 'horizontal'; prompt_words.shift(); await session.send('了解,视频尺寸:【横向】。'); } else if (prompt_words[0] === '竖') { orientation = 'vertical'; prompt_words.shift(); await session.send('了解,视频尺寸:【竖向】。'); } const first_word_after_orientation = prompt_words[0] || ''; const matched_workflow = config.video_workflows.find(w => w.alias === first_word_after_orientation); if (matched_workflow) { final_alias = first_word_after_orientation; prompt_words.shift(); } let final_prompt = prompt_words.join(' '); let target_video_workflow_config; if (final_alias) { target_video_workflow_config = config.video_workflows.find(w => w.alias === final_alias); } else { target_video_workflow_config = config.video_workflows.find(w => w.is_default) || config.video_workflows[0]; } if (!target_video_workflow_config) { return '错误:插件未配置任何【图生视频】工作流,无法处理请求。'; } if (target_video_workflow_config.permission_level > session.user.authority) { return `您的权限不足 (level ${session.user.authority}),无法使用此工作流 (需要 level ${target_video_workflow_config.permission_level})。`; } const needCount = 1; let imageElements = []; if (session.elements) imageElements = imageElements.concat(session.elements.filter(e => e.type === 'image' || e.type === 'img')); if (session.quote && session.quote.elements) imageElements = imageElements.concat(session.quote.elements.filter(e => e.type === 'image' || e.type === 'img')); const queue_item = { type: 'video', session, target_video_workflow_config, prompt_text: final_prompt, use_raw_prompt: options.raw, use_simple_translate: options.translateOnly, orientation, }; if (imageElements.length >= needCount) { request_queue.push({ ...queue_item, imageElements: imageElements.slice(0, needCount) }); await session.send(`你的任务已加入队列,当前排在第 ${request_queue.length} 位。`); logger.info(`新[视频]请求加入队列。当前队列长度: ${request_queue.length}`); process_queue(); return; } const key = getSessionKey(session); pendingImageSessions.set(key, { ...queue_item, images: [], needCount }); await session.send(`请发送1张图片用于转为视频。`); }); // 图片消息监听中间件 ctx.middleware(async (session, next) => { if ((!session.elements || !session.elements.length) && (!session.quote || !session.quote.elements || !session.quote.elements.length)) return next(); const key = getSessionKey(session); const pending = pendingImageSessions.get(key); if (!pending) return next(); let imageElements = []; if (session.elements) imageElements = imageElements.concat(session.elements.filter(e => e.type === 'image' || e.type === 'img')); if (session.quote && session.quote.elements) imageElements = imageElements.concat(session.quote.elements.filter(e => e.type === 'image' || e.type === 'img')); if (!imageElements.length) return next(); pending.images.push(imageElements[0]); if (pending.images.length < pending.needCount) { await session.send(`请发送第${pending.images.length + 1}张图片(共${pending.needCount}张)。`); return; } pendingImageSessions.delete(key); if (request_queue.length >= max_queue_size) { await session.send(`处理队列已满 (最大: ${max_queue_size}),请稍后再试。`); return; } const request_item = { ...pending, imageElements: pending.images, }; request_queue.push(request_item); await session.send(`你的任务已加入队列,当前排在第 ${request_queue.length} 位。`); logger.info(`新[${pending.type}]请求(通过中间件)加入队列。当前队列长度: ${request_queue.length}`); process_queue(); }); // 队列处理主流程 async function process_queue() { if (is_processing || request_queue.length === 0) return; is_processing = true; const task = request_queue.shift(); const { session, type } = task; try { if (type === 'image') { await process_image_task(task); } else if (type === 'video') { await process_video_task(task); } else { throw new Error(`未知的任务类型: ${type}`); } } catch (error) { logger.error(`[${session.guildId || session.userId}] 任务执行失败: ${error.stack}`); await session.send(h('at', { id: session.userId }) + ` 处理失败:${error.message}`); } finally { is_processing = false; logger.info(`一个任务结束,"工人"已空闲。检查队列中是否还有下一个任务...`); process_queue(); } } // 图生图任务处理 async function process_image_task(task) { const { session, target_workflow_config, prompt_text, use_raw_prompt, use_simple_translate, imageElements } = task; logger.info(`开始处理队列中的[图片]任务 (工作流: ${target_workflow_config.alias}),剩余任务数: ${request_queue.length}`); const comfyui_client_id = uuidv4(); const comfyui_address = config.comfyui.server_address; await session.send(h('at', { id: session.userId }) + ' 你的图片任务已开始处理,预计需要约2分钟,请耐心等待...'); const loadedWorkflow = await load_workflow_from_file(target_workflow_config.file_path, target_workflow_config.alias); if (!loadedWorkflow) { throw new Error(`图生图工作流 (别名: ${target_workflow_config.alias}) 加载失败。请检查文件路径和内容。`); } const workflow = JSON.parse(JSON.stringify(loadedWorkflow)); let imageNodeIds = target_workflow_config.load_image_node_ids; if (!Array.isArray(imageNodeIds)) imageNodeIds = [imageNodeIds]; for (let i = 0; i < imageElements.length && i < imageNodeIds.length; ++i) { const imageName = await upload_image_to_comfyui(imageElements[i].attrs.src, comfyui_address); if (!workflow[imageNodeIds[i]] || !workflow[imageNodeIds[i]].inputs) throw new Error(`工作流 "${target_workflow_config.alias}" 中找不到ID为 "${imageNodeIds[i]}" 的图片加载节点。`); workflow[imageNodeIds[i]].inputs.image = imageName; } const user_prompt = prompt_text ? prompt_text.replace(/<img[^>]*>/g, '').trim() : ''; let final_prompt = ''; if (user_prompt) { logger.info(`收到用户提示词: "${user_prompt}"`); if (config.prompt_engineer.enable && !use_raw_prompt) { final_prompt = await process_prompt_with_deepseek(user_prompt, use_simple_translate, session, 'image'); } else { final_prompt = user_prompt; } } else if (target_workflow_config.default_positive_prompt) { logger.info(`用户未提供提示词,使用工作流 "${target_workflow_config.alias}" 的默认提示词。`); final_prompt = target_workflow_config.default_positive_prompt; } else { logger.info(`用户未提供提示词,且工作流未配置默认提示词。`); } set_prompt_in_workflow(workflow, target_workflow_config.positive_prompt_node_id, final_prompt, target_workflow_config.alias); set_random_seed(workflow); const { generated_outputs } = await execute_comfyui_workflow(workflow, comfyui_client_id, comfyui_address, session, config.comfyui.request_timeout); if (generated_outputs.images && generated_outputs.images.length > 0) { const firstImgData = generated_outputs.images[0]; const firstImageUrl = `http://${comfyui_address}/view?filename=${firstImgData.filename}&subfolder=${firstImgData.subfolder}&type=${firstImgData.type}`; await session.send(h('at', { id: session.userId }) + ' 您的图片生成好了!' + h.image(firstImageUrl)); for (let i = 1; i < generated_outputs.images.length; i++) { const imgData = generated_outputs.images[i]; const imageUrl = `http://${comfyui_address}/view?filename=${imgData.filename}&subfolder=${imgData.subfolder}&type=${imgData.type}`; await session.send(h.image(imageUrl)); } } else { throw new Error('未能生成图片,请检查ComfyUI后台或日志。'); } } // 视频任务处理 async function process_video_task(task) { const { session, target_video_workflow_config, prompt_text, use_raw_prompt, use_simple_translate, imageElements, orientation } = task; logger.info(`开始处理队列中的[视频]任务 (工作流: ${target_video_workflow_config.alias}, 尺寸: ${orientation}),剩余任务数: ${request_queue.length}`); const comfyui_client_id = uuidv4(); const comfyui_address = config.comfyui.server_address; await session.send(h('at', { id: session.userId }) + ' 你的视频任务已开始处理,预计需要约6分钟,请耐心等待...'); const loadedWorkflow = await load_workflow_from_file(target_video_workflow_config.file_path, target_video_workflow_config.alias); if (!loadedWorkflow) { throw new Error(`图生视频工作流 (别名: ${target_video_workflow_config.alias}) 加载失败。请检查文件路径和内容。`); } const workflow = JSON.parse(JSON.stringify(loadedWorkflow)); const videoNode = Object.values(workflow).find(node => node.class_type && node.class_type.includes('ImageToVideo')); if (videoNode && videoNode.inputs) { logger.info(`正在设置视频尺寸为: ${orientation}`); if (orientation === 'horizontal') { videoNode.inputs.width = 848; videoNode.inputs.height = 480; } else { videoNode.inputs.width = 480; // [FIXED] 修复了 inputsts 的拼写错误 videoNode.inputs.height = 848; } } else { // [FIXED] 修复了 target_video_workflow_nfig 的拼写错误 logger.warn(`在工作流 "${target_video_workflow_config.alias}" 中未找到包含 'ImageToVideo' 的节点来设置尺寸,将使用工作流默认值。`); } const imageName = await upload_image_to_comfyui(imageElements[0].attrs.src, comfyui_address); const loadImageNodeId = target_video_workflow_config.load_image_node_id; if (!workflow[loadImageNodeId] || !workflow[loadImageNodeId].inputs) { throw new Error(`视频工作流 "${target_video_workflow_config.alias}" 中找不到ID为 "${loadImageNodeId}" 的图片加载节点。`); } workflow[loadImageNodeId].inputs.image = imageName; const user_prompt = prompt_text ? prompt_text.replace(/<img[^>]*>/g, '').trim() : ''; let final_prompt = ''; if (user_prompt) { logger.info(`收到用户视频任务提示词: "${user_prompt}"`); if (config.prompt_engineer.enable && !use_raw_prompt) { final_prompt = await process_prompt_with_deepseek(user_prompt, use_simple_translate, session, 'video'); } else { final_prompt = user_prompt; } } else if (target_video_workflow_config.default_positive_prompt) { logger.info(`用户未提供视频提示词,使用工作流 "${target_video_workflow_config.alias}" 的默认提示词。`); final_prompt = target_video_workflow_config.default_positive_prompt; } else { logger.info(`用户未提供视频提示词,且工作流未配置默认提示词。`); } set_prompt_in_workflow(workflow, target_video_workflow_config.positive_prompt_node_id, final_prompt, target_video_workflow_config.alias); set_random_seed(workflow); const { generated_outputs } = await execute_comfyui_workflow(workflow, comfyui_client_id, comfyui_address, session, config.comfyui.video_request_timeout); const results = (generated_outputs.gifs || []).concat(generated_outputs.images || []); let foundGif = false; if (results.length > 0) { for (const gifData of results) { if (gifData.format === 'image/gif' || (gifData.filename && gifData.filename.endsWith('.gif'))) { const gifUrl = `http://${comfyui_address}/view?filename=${gifData.filename}&subfolder=${gifData.subfolder}&type=${gifData.type}`; if (!foundGif) { await session.send(h('at', { id: session.userId }) + ' 您的视频生成好了!' + h.image(gifUrl)); } else { await session.send(h.image(gifUrl)); } foundGif = true; } } } if (!foundGif) { throw new Error('未能生成或找到GIF格式的输出文件,请检查ComfyUI后台及工作流配置。'); } } // 统一的ComfyUI工作流执行器 async function execute_comfyui_workflow(workflow, client_id, address, session, timeout_seconds) { return new Promise((resolve, reject) => { const ws_address = `ws://${address}/ws?clientId=${client_id}`; let ws; try { ws = new WebSocket(ws_address); } catch (error) { logger.error(`无法创建 WebSocket 连接: ${ws_address}。错误: ${error.message}`); return reject(new Error(`无法连接到 ComfyUI 服务,请检查地址配置和服务器状态。`)); } const timeout = setTimeout(() => { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { ws.close(); } reject(new Error(`请求超时(${timeout_seconds}秒)`)); }, timeout_seconds * 1000); let promptId = null; ws.on('open', async () => { try { const payload = { prompt: workflow, client_id: client_id }; logger.debug(`[${session.guildId || session.userId}] 准备提交工作流, Payload: ${JSON.stringify(payload, null, 2)}`); const response = await axios.post(`http://${address}/prompt`, payload); promptId = response.data.prompt_id; logger.info(`[${session.guildId || session.userId}] 任务已提交, Prompt ID: ${promptId}`); } catch (err) { clearTimeout(timeout); if (ws.readyState === WebSocket.OPEN) ws.close(); reject(new Error(`提交工作流到ComfyUI失败: ${err.message}`)); } }); ws.on('message', (data) => { let message; try { message = JSON.parse(data.toString()); } catch (error) { logger.warn(`收到来自ComfyUI的非JSON格式消息,已忽略: ${data.toString()}`); return; } if (message.type === 'executed' && message.data.prompt_id === promptId) { const output = message.data.output; const is_final_image = output.images && output.images.some(img => img.type === 'output'); const is_final_gif = output.gifs && output.gifs.length > 0; if (is_final_image || is_final_gif) { const result_type = is_final_gif ? 'GIF' : '图片'; logger.info(`[${session.guildId || session.userId}] 任务 ${promptId} 已完成 (找到最终${result_type}输出)`); clearTimeout(timeout); if (ws.readyState === WebSocket.OPEN) ws.close(); resolve({ generated_outputs: output }); } else if (output.images) { logger.debug(`[${session.guildId || session.userId}] 节点 ${message.data.node} 执行完毕,但其输出非最终结果,继续等待...`); } } else if (message.type === 'execution_error' && message.data.prompt_id === promptId) { clearTimeout(timeout); if (ws.readyState === WebSocket.OPEN) ws.close(); const errorDetails = JSON.stringify(message.data.exception_message || message.data, null, 2); logger.error(`ComfyUI执行错误: ${errorDetails}`); const userErrorMessage = Array.isArray(message.data.exception_message) ? message.data.exception_message.join(' ') : '未知错误'; reject(new Error(`ComfyUI 处理时发生错误。后台信息: ${userErrorMessage}`)); } }); ws.on('error', (err) => { clearTimeout(timeout); logger.error(`WebSocket 连接出错: ${err.message}`); reject(new Error(`与 ComfyUI 的连接发生错误: ${err.message}`)); }); ws.on('close', (code, reason) => { clearTimeout(timeout); logger.warn(`WebSocket 连接已关闭,代码: ${code}, 原因: ${reason}`); if (!promptId) { reject(new Error('与 ComfyUI 的连接在任务提交前关闭。')); } }); ws.on('unexpected-response', (request, response) => { clearTimeout(timeout); logger.error(`WebSocket 意外响应: ${response.statusCode}`); reject(new Error(`ComfyUI 服务器返回意外响应: ${response.statusCode}`)); }); }); } // 辅助函数区 async function load_workflow_from_file(filePath, alias) { try { const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(__dirname, filePath); if (!fs.existsSync(absolutePath)) throw new Error(`找不到工作流文件: ${absolutePath}`); const workflow_text = fs.readFileSync(absolutePath, 'utf-8'); if (!workflow_text || workflow_text.trim().length === 0) throw new Error(`工作流文件为空: ${absolutePath}`); const workflow = JSON.parse(workflow_text); if (Object.keys(workflow).length < 2) throw new Error(`工作流文件 "${alias}" 内容似乎无效或已被加密,请检查文件。`); return workflow; } catch (error) { logger.error(`无法读取或解析工作流文件 (别名: ${alias}): ${error.message}`); return null; } } function set_prompt_in_workflow(workflow, nodeId, prompt, alias) { if (prompt && nodeId) { if (!workflow[nodeId] || !workflow[nodeId].inputs) { throw new Error(`工作流 "${alias}" 中找不到ID为 "${nodeId}" 的提示词节点。`); } if (workflow[nodeId].inputs.text !== undefined) { workflow[nodeId].inputs.text = prompt; } else if (workflow[nodeId].inputs.prompt !== undefined) { workflow[nodeId].inputs.prompt = prompt; } else { const potentialTextField = Object.keys(workflow[nodeId].inputs).find(key => typeof workflow[nodeId].inputs[key] === 'string'); if(potentialTextField) { workflow[nodeId].inputs[potentialTextField] = prompt; } else { logger.warn(`在工作流 "${alias}" 的节点 "${nodeId}" 中未找到可用于设置提示词的字段。`); } } } } function set_random_seed(workflow) { for(const node of Object.values(workflow)) { if(node.class_type && (node.class_type.includes('Sampler')) && node.inputs && node.inputs.seed !== undefined) { node.inputs.seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); } } } async function upload_image_to_comfyui(imageUrl, address) { const imageResponse = await ctx.http.get(imageUrl, { responseType: 'arraybuffer' }); const imageName = `${uuidv4()}.png`; const formData = new FormData(); formData.append('image', Buffer.from(imageResponse), { filename: imageName }); formData.append('overwrite', 'true'); await axios.post(`http://${address}/upload/image`, formData, { headers: formData.getHeaders(), timeout: config.comfyui.request_timeout * 1000 }); logger.info(`图片 ${imageName} 上传成功`); return imageName; } async function process_prompt_with_deepseek(prompt, simple_translate, session, taskType = 'image') { if (!config.prompt_engineer.deepseek_api_key) { await session.send('(注意:DeepSeek 功能已启用,但未配置 API 令牌,将使用原始提示词。)'); return prompt; } try { if (simple_translate) { logger.info('检测到 -t 选项,正在使用简单翻译模式...'); const translated_prompt = await translate_simple_text(prompt, config.prompt_engineer.deepseek_api_key); logger.info(`简单翻译成功: "${prompt}" -> "${translated_prompt}"`); return translated_prompt; } else { if (taskType === 'video') { logger.info('正在为视频任务使用 [FusionX] 提示词工程模式...'); const engineered_prompt = await generate_video_fusionx_prompt(prompt, config.prompt_engineer.deepseek_api_key); logger.info(`[FusionX] 提示词工程成功: "${prompt}" -> "${engineered_prompt}"`); return engineered_prompt; } else { logger.info('正在为图片任务使用默认的提示词工程模式...'); const engineered_prompt = await generate_structured_prompt(prompt, config.prompt_engineer.deepseek_api_key); logger.info(`提示词工程成功: "${prompt}" -> "${engineered_prompt}"`); return engineered_prompt; } } } catch (e) { logger.error(`DeepSeek 处理失败: ${e.message}`); await session.send('AI提示词处理失败,将使用原始提示词。'); return prompt; } } async function translate_simple_text(text, apiKey) { const url = 'https://api.deepseek.com/chat/completions'; const system_prompt = 'You are a translation engine. Please translate the following text to English. Output only the translated text, without any explanations or other content.'; const payload = { model: 'deepseek-chat', messages: [{ role: 'system', content: system_prompt },{ role: 'user', content: text }], stream: false }; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }; const response = await axios.post(url, payload, { headers }); if (response.data && response.data.choices && response.data.choices[0].message) { // [FIXED] 修复了 trimim() 的拼写错误 return response.data.choices[0].message.content.trim(); } throw new Error('No translated text found in DeepSeek API response.'); } async function generate_video_fusionx_prompt(text, apiKey) { const url = 'https://api.deepseek.com/chat/completions'; const payload = { model: 'deepseek-chat', messages: [{ role: 'system', content: FUSIONX_VIDEO_PROMPT },{ role: 'user', content: text }], stream: false }; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }; const response = await axios.post(url, payload, { headers }); if (response.data && response.data.choices && response.data.choices[0].message) { return response.data.choices[0].message.content.trim(); } throw new Error('No FusionX prompt found in DeepSeek API response.'); } async function generate_structured_prompt(text, apiKey) { const url = 'https://api.deepseek.com/chat/completions'; const system_prompt = ` ## 🔧 基础提示规则 (rule) - 所有修改必须明确、具体,避免使用模糊词汇(如"美美化"、"变好看") - 所有复杂变动必须拆多个步骤进行描述 - 指明哪些元素不变(角色特征、姿势、位置等)- 修改动词必须使用 change / replace / convert / transform --- ## ✏️ 基础修改类 (Basic Modification) 模板: Change [object] to [new state], keep [elements_to_preserve] unchanged 示例: - Change the car color to red - Change the time to daytime while maintaining the same style of the painting. --- ## 🎨 风格转换类 (Style Conversion) 模板: Transform to [specific style], while maintaining [elements_to_preserve] 示例: - Transform to Bauhaus art style - Convert to oil painting with visible brushstrokes and thick paint texture - Convert to pencil sketch with natural graphite lines, cross-hatching, and visible paper texture --- ## 👤 角色一致性类 (Character Consistency) 模板: Change [aspect] of the character to [new state], while maintaining [facial features / hairstyle / pose / expression]] 示例: - Change the clothes to be a viking warrior while preserving facial features - Update the background to a forest while keeping the woman with short black hairin the same pose and expression --- ## 🌅 背景替换类 (Background Change) 模板: Change the background to [new_background],], keep the subject in the exact same [position / pose / scale] 示例: - Change the background to a beach while keeping the person in the exact same position,cale, and pose --- ## 🔠 文本编辑类 (Text Replacement) 模板: Replace '[original_text]' with '[new_text]', maintain the same [font style / layout] 示例: - Replace 'joy' with 'BFL', keeping the font style unchanged --- ## 🪜 分步骤编辑建议 (Multi-Step Instruction) Step 1: Change [background / lighting], keep the character in the same pose Step 2: Change [clothing / expression / item], maintain facial features and hairstyle Step 3: Transform to [desired art style], while preserving the entire composition --- ## ❌ 常见错误与修正建议 (Common Mistakes) 错误: Transform the person into a Viking ✅ 替代: Change the clothes to be a viking warrior while preserving facial features 错误: Put him on a beach ✅ 替代: Change the background to a beach while keeping the person in the exact same position, scale, and pose 错误: Make it a sketch ✅ 替代: Convert to pencil sketch with natural graphite lines, cross-hatching, and visible paper texture --- # 💡 核心记忆口诀: - change:用于修改具体物体属性 - transform:用于风格变化或视觉风格替换 - replace:用于文本替换 - maintain / keep:强调保留特征或构图元素 ## 输出示例 - 用户输入:把这个汽车的颜色变成红色 - 响应输出:Change the car color to red # 注意事项 - 根据用户输入进行规则改写 - 不能输出与用户输入无关的内容 - 输出直接给出标准结果,并且是一段话,不要分段列举列举以及加入备注等。 以上是提示词的撰写要求,你需要按照要求撰写提示词,不可以忘记以上要求。 现在我开始说需求,你用一段话写出提示词,要求英语,并且不要分段列举以及加入备注等。 `; const payload = { model: 'deepseek-chat', messages: [{ role: 'system', content: system_prompt },{ role: 'user', content: text }], stream: false }; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }; const response = await axios.post(url, payload, { headers }); if (response.data && response.data.choices && response.data.choices[0].message) { return response.data.choices[0].message.content.trim(); } throw new Error('No structured prompt found in DeepSeek API response.'); } };