拓冰建站拓冰建站
首页 / 资讯中心 / 正文

03.01.03.ComfyUI:环境搭建篇(集成 AnythingLLM调用ComfyUI的API 使用Custom Skills方式 图生图)

总操作流程1、写代码2、配置3、测试这里使用模型ollama run huihui_ai/qwen3-vl-abliterated:8b-instruct使用 ComfyUI模型权重: Z-Image-Turbo参考02.04.02.ComfyUI环境搭建篇安装 图生图 模型权重 Z-Image-Turbo/02.04.02.ComfyUI环境搭建篇安装模型权重 qwen_3_4b写代码mkdir-p/root/.config/anythingllm-desktop/storage/plugins/agent-skillscd/root/.config/anythingllm-desktop/storage/plugins/agent-skills# 技能的元数据名称、描述、入参等mkdir-pcomfyui-image-generatorcatcomfyui-image-generator/plugin.jsonEOF { active: true, hubId: comfyui-image-generator, name: ComfyUI Image Generator, schema: skill-1.0.0, version: 2.1.0, description: Generate an image or transform an uploaded/local input image with the Z-Image-Turbo-Fun-Controlnet-Union model in ComfyUI. For image-to-image requests, use a chat upload, pass image_path, or include an absolute image path in the prompt; the skill uploads it and applies the official Z-Image Union ControlNet workflow., author: local, license: MIT, setup_args: { COMFYUI_URL: { type: string, required: true, input: { type: text, default: http://10.3.11.174:8188, placeholder: http://10.3.11.174:8188, hint: ComfyUI server URL }, value: http://10.3.11.174:8188 } }, examples: [ { prompt: 基于本地照片生成彩色插画保留原始构图, call: {\prompt\:\colorized detailed illustration based on the input image, preserve the original composition\,\image_path\:\/mnt/D/images.png\,\control_mode\:\canny\,\control_strength\:0.85} }, { prompt: 根据本地照片生成相似风格的新图, call: {\prompt\:\create a new image in a similar style based on the input image\,\image_path\:\/mnt/D/images.png\,\control_mode\:\raw\,\control_strength\:0.65} }, { prompt: 生成一张雨夜上海街头的电影感照片, call: {\prompt\:\cinematic photo of a rainy night street in Shanghai, neon reflections, highly detailed\,\width\:1024,\height\:1024} } ], entrypoint: { file: handler.js, params: { prompt: { description: Detailed English image prompt. Translate and enrich non-English requests before calling., type: string }, image_path: { description: Optional absolute path to an input image, for example /mnt/D/images.png. Use this for colorization or image-to-image requests., type: string }, control_mode: { description: Optional input-image mode: canny preserves edges and composition; raw uses the uploaded image directly. Default canny., type: string }, control_strength: { description: Optional Z-Image Union ControlNet strength from 0 to 2. Use 0.7 to 1.0 for structure-preserving image-to-image., type: number }, negative_prompt: { description: Optional things to exclude from the image, type: string }, width: { description: Optional image width in pixels, type: number }, height: { description: Optional image height in pixels, type: number }, steps: { description: Optional sampling steps, 1 to 50; default 8 for Z-Image Turbo, type: number }, seed: { description: Optional integer seed for reproducible output, type: number } } }, imported: true } EOF# 具体的 NodeJS 执行逻辑catcomfyui-image-generator/handler.jsEOF const fs require(fs); const path require(path); module.exports.runtime { handler: async function ({ prompt, negative_prompt blurry, low quality, distorted, deformed, watermark, text, width 1024, height 1024, steps 8, control_strength 1, control_mode canny, seed, image_path, image_data, image_name, }) { const callerId ${this.config.name}-v${this.config.version}; try { const baseUrl String(this.runtimeArgs.COMFYUI_URL || http://10.3.11.174:8188).replace(/\/$/, ); if (!prompt || !String(prompt).trim()) return Image generation failed: prompt is required.; // Accept the common agent form: ... image at /mnt/D/images.png. if (!image_path) { const match String(prompt).match(/(\/(?:[^\s]|\\ )\.(?:png|jpe?g|webp|bmp))(?$|[\s])/i); if (match) { image_path match[1].replace(/\\ /g, ); prompt String(prompt).replace(match[0], ).replace(/\s{2,}/g, ).trim(); } } width this._dimension(width); height this._dimension(height); steps Math.max(1, Math.min(50, Math.round(Number(steps) || 8))); control_strength Math.max(0, Math.min(2, Number(control_strength) || 1)); control_mode [canny, raw].includes(String(control_mode).toLowerCase()) ? String(control_mode).toLowerCase() : canny; seed Number.isSafeInteger(Number(seed)) ? Math.max(0, Number(seed)) : Math.floor(Math.random() * 2147483647); // AnythingLLM keeps a chat upload as a data URL on the Agent conversation. // The model often omits image_path in the tool call, so recover that attachment here. if (!image_path !image_data) { const attachment this._findImageAttachment(this); if (attachment) { image_data attachment.contentString; image_name attachment.name; } } let uploadedImage; let temporaryInput; if (!image_path image_data) { temporaryInput await this._writeDataUrl(image_data, image_name); image_path temporaryInput; } if (image_path) { const source String(image_path).trim(); if (!path.isAbsolute(source)) throw new Error(image_path must be an absolute local path); if (!fs.existsSync(source)) throw new Error(Input image does not exist: ${source}); this.introspect(${callerId}: uploading input image to ComfyUI...); uploadedImage await this._uploadImage(baseUrl, source); if (temporaryInput) await fs.promises.unlink(temporaryInput).catch(() {}); } this.introspect(${callerId}: submitting a ${width}x${height} image to ComfyUI...); const workflow this._workflow({ prompt: String(prompt), negativePrompt: String(negative_prompt || ), width, height, steps, controlStrength: control_strength, controlMode: control_mode, seed, uploadedImage }); const queued await this._json(${baseUrl}/prompt, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt: workflow, client_id: anythingllm-${Date.now()} }), }, 15000); if (!queued.prompt_id) throw new Error(queued.error?.message || ComfyUI did not return a prompt_id); this.introspect(${callerId}: ComfyUI is generating the image...); const history await this._waitForResult(baseUrl, queued.prompt_id, 300000); const images Object.values(history.outputs || {}).flatMap((output) output.images || []); if (!images.length) throw new Error(history.status?.messages?.map((item) JSON.stringify(item)).join(; ) || ComfyUI completed without an image); const markdown images.map((image, index) { const params new URLSearchParams({ filename: image.filename, subfolder: image.subfolder || , type: image.type || output }); return ![Generated image ${index 1}](${baseUrl}/view?${params.toString()}); }).join(\n\n); this.introspect(${callerId}: image generation completed.); return Image generated successfully. Show the following Markdown exactly as written in your final response (do not put it in a code block):\n\n${markdown}\n\nSeed: ${seed}; size: ${width}x${height}; control: ${control_mode}.; } catch (error) { this.logger(${callerId} failed: ${error.message}); this.introspect(${callerId}: generation failed: ${error.message}); return Image generation failed: ${error.message}; } }, _dimension(value) { const number Math.max(256, Math.min(1536, Number(value) || 1024)); return Math.round(number / 16) * 16; }, async _uploadImage(baseUrl, filename) { const data await fs.promises.readFile(filename); const form new FormData(); form.append(image, new Blob([data], { type: application/octet-stream }), path.basename(filename)); form.append(overwrite, true); const response await fetch(${baseUrl}/upload/image, { method: POST, body: form, signal: AbortSignal.timeout(30000) }); const text await response.text(); let result; try { result JSON.parse(text); } catch { throw new Error(ComfyUI upload returned HTTP ${response.status}: ${text.slice(0, 300)}); } if (!response.ok || !result.name) throw new Error(result.error || ComfyUI upload failed with HTTP ${response.status}); return result.subfolder ? ${result.subfolder}/${result.name} : result.name; }, _findImageAttachment(context) { const pools [ context?.attachments, context?.super?.attachments, context?.handlerProps?.attachments, context?.super?.handlerProps?.attachments, context?.super?.chats, ]; for (const pool of pools) { if (!Array.isArray(pool)) continue; for (let index pool.length - 1; index 0; index - 1) { const item pool[index]; const attachments Array.isArray(item?.attachments) ? item.attachments : [item]; const image attachments.find((candidate) String(candidate?.mime || candidate?.type || ).startsWith(image/) candidate?.contentString); if (image) return image; } } return null; }, async _writeDataUrl(dataUrl, name anythingllm-input.png) { const match String(dataUrl).match(/^data:[^;];base64,(.)$/s); if (!match) throw new Error(Attached image is not a valid data URL); const safeName path.basename(String(name || anythingllm-input.png)).replace(/[^a-zA-Z0-9._-]/g, _); const filename /tmp/anythingllm-comfyui-${Date.now()}-${safeName}; await fs.promises.writeFile(filename, Buffer.from(match[1], base64)); return filename; }, _workflow({ prompt, negativePrompt, width, height, steps, controlStrength, controlMode, seed, uploadedImage }) { // Official Z-Image-Turbo-Fun-Controlnet-Union graph: base UNet model patch. const workflow { 1: { class_type: UNETLoader, inputs: { unet_name: z_image_turbo_bf16.safetensors, weight_dtype: default } }, 2: { class_type: CLIPLoader, inputs: { clip_name: qwen_3_4b.safetensors, type: lumina2 } }, 3: { class_type: VAELoader, inputs: { vae_name: ae.safetensors } }, 4: { class_type: ModelPatchLoader, inputs: { name: Z-Image-Turbo-Fun-Controlnet-Union.safetensors } }, 6: { class_type: ModelSamplingAuraFlow, inputs: { model: [uploadedImage ? 5 : 1, 0], shift: 3 } }, 7: { class_type: CLIPTextEncode, inputs: { text: prompt, clip: [2, 0] } }, 8: { class_type: ConditioningZeroOut, inputs: { conditioning: [7, 0] } }, 9: { class_type: EmptySD3LatentImage, inputs: { width, height, batch_size: 1 } }, 10: { class_type: KSampler, inputs: { model: [6, 0], seed, steps: Math.min(50, Math.max(1, steps)), cfg: 1, sampler_name: res_multistep, scheduler: simple, positive: [7, 0], negative: [8, 0], latent_image: [9, 0], denoise: 1 } }, 11: { class_type: VAEDecode, inputs: { samples: [10, 0], vae: [3, 0] } }, 12: { class_type: SaveImage, inputs: { images: [11, 0], filename_prefix: AnythingLLM/ZImage } }, }; if (uploadedImage) { workflow[5] { class_type: ZImageFunControlnet, inputs: { model: [1, 0], model_patch: [4, 0], vae: [3, 0], strength: controlStrength } }; workflow[13] { class_type: LoadImage, inputs: { image: uploadedImage } }; workflow[14] { class_type: ImageScaleToMaxDimension, inputs: { image: [13, 0], upscale_method: lanczos, largest_size: Math.min(1024, Math.max(width, height)) } }; if (controlMode canny) { workflow[15] { class_type: Canny, inputs: { image: [14, 0], low_threshold: 0.1, high_threshold: 0.32 } }; workflow[5].inputs.image [15, 0]; workflow[5].inputs.inpaint_image [14, 0]; } else { workflow[5].inputs.image [14, 0]; workflow[5].inputs.inpaint_image [14, 0]; } } return workflow; }, async _json(url, options {}, timeoutMs 10000) { const response await fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) }); const text await response.text(); let data; try { data JSON.parse(text); } catch { throw new Error(ComfyUI returned HTTP ${response.status}: ${text.slice(0, 300)}); } if (!response.ok) throw new Error(data.error?.message || data.error || ComfyUI HTTP ${response.status}); return data; }, async _waitForResult(baseUrl, promptId, timeoutMs) { const deadline Date.now() timeoutMs; while (Date.now() deadline) { const data await this._json(${baseUrl}/history/${encodeURIComponent(promptId)}, {}, 10000); if (data[promptId]) return data[promptId]; await new Promise((resolve) setTimeout(resolve, 1500)); } throw new Error(ComfyUI generation timed out after 5 minutes); }, }; EOFchmod0777-R/root/.config/anythingllm-desktop/storage/plugins/agent-skillschown$USER:$USER-R/root/.config/anythingllm-desktop/storage/plugins/agent-skills启动# 重启服务器sudo-upostgres /usr/local/software/postgresql/data/pgsql/bin/pg_ctl restart-D/usr/local/software/postgresql/data/pgdata-l/usr/local/software/postgresql/data/pglog/logfile-mf# 启动ComfyUIcd/usr/local/software/ComfyUI/sourcevenv/bin/activate python main.py--listen0.0.0.0--port8188# 启动AnythingLLMDesktop/usr/local/software/AnythingLLMDesktop/anythingllm-desktop --no-sandbox测试画图# 图生图agent 给该图片上色彩。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门