
Stable Diffusion 本地玩得再开心也只是一个人的游戏。真正让它在团队里发光还得靠一个所有人都能伸手就用的入口。我这段时间折腾的 AIYA就是把 Stable Diffusion 包装成一个 Discord 机器人接口群友在聊天框里发一条斜杠命令机器人排队调显卡生成后把图直接丢回群里。很多人第一反应是“这不就是套壳”但实际做下来并发、交互、网络、资源管理这些问题一点都不少。这篇文章会把 AIYA 从零到落地整个拆开包括架构、关键代码、部署和踩坑记录给想自己做同类的朋友一份能直接抄作业的参考。AIYA 解决的痛点很明确不是所有人都愿意打开 Stable Diffusion WebUI 一点点调参也不是每个人都有能跑 SD 的显卡。把它放在 Discord 上之后生成图片变成了一次聊天交互命令天然带参数图片结果有存档讨论也都在频道里比截图传文件高效得多。下面我按自己的实现顺序从设计思路、任务调度、具体代码到常见故障排查一条条说清楚。1. 项目概述与设计思路1.1 为什么要给 Stable Diffusion 做 Discord 接口大多数人一开始用 SD 都是在本地开 WebUI自己玩没问题但一旦同事朋友也想“来一张”麻烦就来了。要么把 WebUI 暴露到公网要么让对方自己装一套环境结果显存不够、模型版本不一致、想复现同一张图还得一层层对参数折腾一下午效率全没了。Discord 机器人能把这套流程收敛成一次聊天操作大家在同一个频道里互相参考 prompt机器人统一排队出图后还能直接回复调整参数这种体验比截图沟通自然得多。另一个很实际的原因是人多之后显卡资源需要分配。如果几个人同时点生成WebUI 会同时开好几个推理任务显存很容易被撑爆轻则报错重则整卡挂掉。AIYA 做了一层调度器所有请求进队列一次只让一张卡干一件事出完图再接下一条。这个设计不是可有可无的装饰而是保证多人场景下机器人能持续干活的根基。1.2 需求拆解一个机器人要管哪些事动手写代码前我先把需求拆成了清单。AIYA 至少要覆盖以下这些事情接收用户输入的 prompt 和常用参数比如步数、尺寸、种子、CFG。对这些参数做合法性校验非法值直接提示而不是甩给后端报错。把任务放进全局队列保证同时只有一个训练或生成任务占用显卡。生成成功之后把图片作为 Discord 消息附件发送失败时告诉用户失败原因。管理员能限制每个用户的使用频率防止有人恶意刷图把显卡打满。提供基本的状态查询方便知道当前排了几条任务、显卡是否空闲。第 4 点和第 5 点很容易被忽略。很多人第一次做机器人只写了“接收指令、调用 API、返回图片”这三步上线后才发现某个人连着发二十条指令整台服务器卡死或者 SD WebUI 已经出错但机器人还在傻等最后用户看到的是“命令无响应”。把异常处理和频率控制放在需求阶段后面会省很多事。1.3 技术选型调用 SD WebUI API还是自己写推理AIYA 的后端生成没有自己写采样器而是直接调用 Stable Diffusion WebUI 自带的 HTTP API。原因很简单WebUI 已经帮我们处理了模型加载、VAE、采样器、Lora 这些底层细节我只需要向/sdapi/v1/txt2img发一个 POST 请求传进常规参数就能拿到图。这样做风险低、文档全、排查也容易。如果换成 ComfyUI也能做但要在 Python 代码里维护 workflow JSON参数映射更繁琐适合本来就深度使用 ComfyUI 的团队。AIYA 的目标是“把 SD 能力快速变成多人可用的服务”所以我优先选了最稳妥的路径。机器人框架方面我用的是 py-cord而不是老牌的 discord.py。py-cord 对斜杠命令的支持更直观bot.slash_command装饰器直接声明参数和类型Discord 客户端会自动生成参数输入框比解析纯文本消息可靠得多。网络请求用httpx.AsyncClient因为它是异步库不会像requests那样阻塞整个机器人事件循环。2. 核心原理与架构拆解2.1 一条指令从发出到出图的完整链路AIYA 的一次完整生成流程是这样的用户在 Discord 输入/aiya prompta silver fox in cyberpunk desert steps40Discord 服务器把这条命令事件推送给机器人进程。机器人先调用ctx.defer()告诉 Discord “我收到了正在处理”避免客户端显示操作失败。紧接着机器人把参数打包成一个任务对象放入asyncio.Queue这一步是瞬时的不会卡住交互。后台有一个常驻的 worker 协程等待队列里出现任务。拿到任务后它会向本机 Stable Diffusion WebUI 的/sdapi/v1/txt2img接口发送请求。WebUI 完成推理后返回 base64 编码的图片worker 解码保存到本地文件最后再通过filediscord.File(...)把图片作为消息附件发送到频道里。整个过程可以用一张表格概括每个环节的职责环节职责关键技术点Discord 网关接收命令、回调交互斜杠命令、Application CommandBot 交互层解析参数、返回状态ctx.defer、ctx.respond任务队列排队、限制并发asyncio.Queue、SemaphoreSD 后端执行实际推理SD WebUI HTTP API文件回传保存图片并发送Discord File 附件这个链路里最容易出问题的不是 SD 本身而是机器人异步逻辑。如果直接用同步代码执行 WebUI 请求整个机器人事件循环会被阻塞其他人发指令也没反应。所以从第二步开始所有耗时操作都必须放在异步函数里。2.2 异步任务队列与多用户并发Stable Diffusion WebUI 的常见部署方式是单进程API 同时接收到多个请求时会各自尝试加载模型并占有显存最终出现CUDA out of memory。解决办法不是把显卡拆开而是让所有任务串行化。AIYA 里我维护了一个asyncio.Queue任务生产者和消费者完全解耦。用户在斜杠命令里提交任务只是往队列里塞了一个带有asyncio.Future的任务对象worker 消费队列完成后把结果写回 Future。这样用户协程可以await这个 Future既能拿到结果又有超时控制。核心结构大致是这样class TaskManager: def __init__(self, concurrency: int 1): self.queue asyncio.Queue(maxsize100) self.semaphore asyncio.Semaphore(concurrency) self.worker_task asyncio.create_task(self._worker()) async def submit_and_wait(self, params: dict, timeout: int 240): loop asyncio.get_running_loop() future loop.create_future() job {params: params, future: future} try: self.queue.put_nowait(job) except asyncio.QueueFull: raise RuntimeError(当前排队任务已满请稍后再试) return await asyncio.wait_for(future, timeouttimeout) async def _worker(self): while True: job await self.queue.get() async with self.semaphore: try: result await generate_image(job[params]) if not job[future].done(): job[future].set_result(result) except Exception as exc: if not job[future].done(): job[future].set_exception(exc) finally: self.queue.task_done()为什么用asyncio.Future而不是直接回调因为 Future 可以安全地跨协程传递结果用户协程在await时会被挂起等 worker 拿到结果后再唤醒。队列满的时候用put_nowait抛出QueueFull直接告诉用户“排队满了”而不是无限等待。2.3 资源控制并发限制、频率限制与超时策略队列本身只解决了“任务有序”还需要配合并发信号量控制同时推理的数量。AIYA 默认concurrency1也就是同一时刻只允许一个 SD 任务在跑。如果显卡显存比较大、模型比较小可以调整成 2但要注意 WebUI 后端多开任务时的稳定性。我测试下来concurrency1是最保守也最不容易出问题的选择。频率限制我做了用户维度。每个用户每 30 秒只能提交一次生成任务防止一个人连发命令刷爆队列。简单实现如下import time last_used: dict[int, float] {} async def check_rate_limit(user_id: int): now time.monotonic() interval now - last_used.get(user_id, 0) if interval 30: raise RuntimeError(生成频率太快请 30 秒后再试) last_used[user_id] now超时策略也很重要。SD WebUI 偶尔会卡死HTTP 请求无限挂起。我在调用生成 API 的httpx.AsyncClient里设置了 300 秒的超时同时给用户协程的 Future 设置 240 秒的等待上限。一旦 Future 超时用户会收到失败提示worker 那边的逻辑在下一次循环里自然丢弃异常结果不会影响后面的任务。3. 从零落地AIYA 的具体实现与部署3.1 准备环境SD WebUI 开启 API 模式AIYA 运行在一台带 NVIDIA GPU 的 Linux 服务器上。先安装好显卡驱动和 CUDA再按官方方式安装 Stable Diffusion WebUI。启动 WebUI 时一定要带上--api参数否则/sdapi接口不会开放./webui.sh --api --listen 127.0.0.1 --port 7860 --xformers --medvram各参数的含义--api开启 HTTP API--listen 127.0.0.1表示只监听本机地址因为机器人和 WebUI 在同一台机器上不需要把端口暴露到公网--xformers开启内存优化--medvram是中等显存优化类似情况下很管用。如果使用 Windows 上的 webui-forge 启动脚本run.bat卡在installing requirements这类阶段通常和依赖下载网络有关系可以多等一会儿或者手动安装对应的requirements_versions.txt。接下来要创建 Discord 机器人。在 Discord Developer Portal 新建 Application添加 Bot复制 Token。邀请机器人到服务器时需要勾选applications.commands作用域并赋予发送消息、上传文件、使用应用命令这几项权限。Token 要保存好后续通过环境变量读取不要硬编码进代码里。3.2 编写机器人Slash 命令、参数解析与调度器AIYA 的主体代码放在/opt/aiya/main.py下。机器人使用 py-cord 声明斜杠命令参数直接用 Discord 的 Option 类型定义这样用户输入时客户端会自动校验类型和范围。import uuid import discord from discord.ext import commands from task_manager import TaskManager bot commands.Bot(command_prefix!, intentsdiscord.Intents.default()) task_manager TaskManager(concurrency1) bot.slash_command(nameaiya, description调用 Stable Diffusion 生成图片) async def aiya( ctx: discord.ApplicationContext, prompt: discord.Option(str, 提示词支持英文和自然语言), negative: discord.Option(str, 负面提示词, default), steps: discord.Option(int, 采样步数, default30, min_value10, max_value80), width: discord.Option(int, 宽度, default768, min_value512, max_value1536), height: discord.Option(int, 高度, default768, min_value512, max_value1536), seed: discord.Option(int, 随机种子-1 表示随机, default-1), cfg: discord.Option(float, CFG Scale, default7.0, min_value1.0, max_value30.0), ): await ctx.defer() try: params { id: uuid.uuid4().hex, prompt: prompt, negative_prompt: negative, steps: steps, width: width, height: height, seed: seed, cfg_scale: cfg, } result await task_manager.submit_and_wait(params, timeout240) await ctx.respond(filediscord.File(result[path])) except Exception as exc: embed discord.Embed(title生成失败, descriptionstr(exc), color0xe74c3c) await ctx.respond(embedembed)这里最容易被忽略的是await ctx.defer()。Discord 的交互响应只有 3 秒窗口如果 SD 生成需要半分钟不先 defer客户端早就显示“交互失败”了。defer 之后后续ctx.respond或ctx.followup.send都可以继续使用这次交互。Slash 命令的好处是参数结构固定。不需要自己写解析--steps这类参数的逻辑Discord 客户端会自动处理空格和引号prompt 内容也不容易被截断。如果后面想兼容普通聊天消息再单独写一个解析器也不迟。3.3 接入生成服务调用 SD WebUI 的 txt2img 接口生成函数的实现是把任务参数组装成 WebUI API 需要的 JSON发送到/sdapi/v1/txt2img然后把返回的 base64 图片解码保存。import base64 import os import httpx async def generate_image(params: dict): api_url http://127.0.0.1:7860/sdapi/v1/txt2img payload { prompt: params[prompt], negative_prompt: params.get(negative_prompt, ), steps: params.get(steps, 30), width: params.get(width, 768), height: params.get(height, 768), cfg_scale: params.get(cfg_scale, 7.0), seed: params.get(seed, -1), sampler_name: DPM 2M Karras, } async with httpx.AsyncClient(timeout300) as client: resp await client.post(api_url, jsonpayload) resp.raise_for_status() data resp.json() image_b64 data[images][0] path os.path.join(generated, f{params[id]}.png) os.makedirs(generated, exist_okTrue) with open(path, wb) as f: f.write(base64.b64decode(image_b64)) return {path: path, info: data.get(info, )}需要说明两点。第一images是一个 base64 字符串列表常规 txt2img 只需要取第一个如果 WebUI 开启了批量生成会有多个。第二sampler_name不同版本的 WebUI 有轻微差异比如DPM 2M Karras是常用选项如果 API 报采样器不存在可以在 WebUI 的界面里把选中采样器的内部名称抄回来。返回的info字段里包含这次生成的真实参数和随机种子需要做“一键复现”时可以把info解析并存到数据库。3.4 部署与守护用 systemd 管理机器人进程本地调试没问题后把代码放到服务器目录。我习惯的目录结构是/opt/aiya ├── main.py ├── task_manager.py ├── config.py └── generated/运行机器人时不能只靠nohup因为进程崩了没人会手动拉起。我给 AIYA 写了一个 systemd 服务[Unit] DescriptionAIYA Discord Bot Afternetwork-online.target [Service] Userubuntu WorkingDirectory/opt/aiya EnvironmentDISCORD_TOKEN你的机器人Token ExecStart/usr/bin/python3 /opt/aiya/main.py Restarton-failure RestartSec5 [Install] WantedBymulti-user.target启动命令sudo systemctl daemon-reload sudo systemctl enable --now aiya systemctl status aiya日志用journalctl -u aiya -f查看。如果 Bot 进程崩溃Restarton-failure会在 5 秒后重新拉起来。SD WebUI 也要保证开机自启否则机器人还在跑但后端推理接口挂了所有生成任务都会失败。我建议把 WebUI 也做成一个 service并用Aftersd-webui.service指定启动顺序。4. 常见问题与排查技巧实录4.1 机器人无法上线或命令不响应机器人不上线最常见的原因是 Token 错误或网络不通。先检查代码启动日志里有没有Logged in as字样。如果一直停留在连接阶段可以用命令行测一下网关curl -I https://discord.com/api/v10/gateway正常会返回 200 或 400 系列的响应。如果超时或连接失败说明运行环境和 Discord API 之间的网络链路有问题。这种问题通常不是代码 bug而是部署机器的网络访问条件受限。建议把机器人部署在网络链路正常、能稳定访问 Discord API 的服务器上保证 HTTPS 出站畅通这也是最合规稳妥的做法。命令不响应还有一个很隐蔽的原因邀请机器人时没有勾选applications.commands作用域导致斜杠命令根本没注册到服务器。去 Developer Portal 重新生成邀请链接勾上这个 scope再用新链接邀请一次就好了。4.2 并发出图导致 CUDA 显存不足我上线后第一个崩溃场景是群里三个人同时点生成WebUI 直接报CUDA out of memory。原因就是并发限制没生效多个 HTTP 请求同时进入 SD API。解决办法是把TaskManager的concurrency参数设为 1同时加上 WebUI 启动时的--medvram参数。如果还是爆显存可以考虑降低默认出图分辨率。AIYA 默认是 768x768对 6GB 显存的卡已经有点紧张。可以按显卡实际情况把默认调整到 512x768再把--lowvram加上。限制用户侧频率也有用防止单个人在短时间内批量提交 20 个任务。这里有一个细节Semaphore 控制的只是 AIYA 进程内的并发。如果外部有人直接访问 WebUI 的 API 端口绕过 AIYA 也能触发推理。所以我始终在 WebUI 启动参数里用--listen 127.0.0.1只允许本机调用。4.3 WebUI 返回 500、黑图、采样器报错速查SD WebUI 的 API 并不是时刻都稳定常见报错基本可以归纳成下面几类现象可能原因处理方式HTTP 500 Internal Server ErrorWebUI 进程显存爆掉或模型加载失败查看 WebUI 日志确认模型路径、显存占用图片全黑或满是噪点CFG 过高、采样器不支持把 CFG 调到 7 左右换用 DPM 2M KarrasModuleNotFoundErrorPython 环境依赖缺失使用 WebUI 自带的 venv 启动或手动安装对应 requirementsRuntimeError: CUDA error驱动或显存异常降低分辨率、开启 medvram重启服务器返回图片尺寸不对传入参数被 WebUI 拒绝检查 width/height 是否为 64 的倍数如 768x768 是800x600 不是这里要特别提醒width和height必须是 64 的倍数。用户传 800x600 时WebUI API 可能会返回一个奇怪的编解码错误。AIYA 在参数校验时就约束了步长前端只能选 512、768、1024 这类数值。采样器名字踩坑概率也很大。不同 WebUI 版本内置的 sampler 名称不完全一样最好先从/sdapi/v1/samplers拉一份当前环境支持的列表再用这份列表校验参数而不是像我第一次那样写死一个字符串。4.4 关于 Discord 交互超时和图片大小限制的避坑Discord 对交互响应有严格的时间限制如果不调用defer3 秒内没响应就会失败调用了defer之后还有 15 分钟的窗口可以继续回消息。所以我在所有生成型命令里第一件事就是await ctx.defer()然后才进入队列。另一个限制是附件大小。Discord 普通服务器单个文件上限是 8MBStable Diffusion 生成的高分辨率 PNG 很容易超过这个值。我的处理是在发送前用 Pillow 压缩成 JPEGfrom PIL import Image def compress_to_jpeg(path: str, quality: int 85): img Image.open(path).convert(RGB) out_path path.rsplit(., 1)[0] .jpg img.save(out_path, JPEG, qualityquality) return out_path压缩后的图片体积会小很多画质损失肉眼几乎看不出来。生成目录也要定期清理否则长时间跑下来磁盘会被大量 PNG 填满。我设了一个定时任务删除两天前的生成文件避免服务器出空间问题。最后分享一个我自己的小习惯在机器人里加一个/status命令返回当前队列长度、正在生成的任务数量、WebUI 是否在线。排查“命令明明提交了但没出图”这类问题时会非常有用不用每次都去翻 WebUI 日志直接在 Discord 里就能看到任务卡在哪个环节。AIYA 的核心其实不在 Stable Diffusion 本身而在“调度”和“交互”这两个工程细节上把这两件事做扎实机器人才能真正从玩具变成工具。