03.01.03.ComfyUI:环境搭建篇(集成 AnythingLLM调用ComfyUI的API 使用Custom Skills方式 图生图)
2026/9/3 7:25:41 网站建设 项目流程

总操作流程:

  • 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-generatorcat>comfyui-image-generator/plugin.json<<'EOF' { "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 执行逻辑cat>comfyui-image-generator/handler.js<<'EOF' 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 给该图片上色彩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询