Files
comfy-discord-web/DEVELOPMENT.md
Khoa (Revenovich) Tran Gia 1ed3c9ec4b Initial commit — ComfyUI Discord bot + web UI
Full source for the-third-rev: Discord bot (discord.py), FastAPI web UI
(React/TS/Vite/Tailwind), ComfyUI integration, generation history DB,
preset manager, workflow inspector, and all supporting modules.

Excluded from tracking: .env, invite_tokens.json, *.db (SQLite),
current-workflow-changes.json, user_settings/, presets/, logs/,
web-static/ (build output), frontend/node_modules/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:55:48 +07:00

20 KiB

Development Guide

This guide explains how to add new commands, features, and modules to the Discord ComfyUI bot.

Table of Contents


Adding a New Command

Commands are organized in the commands/ directory by functionality. Here's how to add a new command:

Step 1: Choose the Right Module

Determine which existing command module your command belongs to:

  • generation.py - Image/video generation commands
  • workflow.py - Workflow template management
  • upload.py - File upload commands
  • history.py - History viewing and retrieval
  • workflow_changes.py - Runtime workflow parameter management

If none fit, create a new module (see Adding a New Feature Module).

Step 2: Add Your Command Function

Edit the appropriate module in commands/ and add your command to the setup_*_commands() function:

# commands/generation.py

def setup_generation_commands(bot, config):
    # ... existing commands ...

    @bot.command(name="my-new-command", aliases=["mnc", "my-cmd"])
    @require_comfy_client  # Use this decorator if you need bot.comfy
    async def my_new_command(ctx: commands.Context, *, args: str = "") -> None:
        """
        Brief description of what your command does.

        Usage:
            ttr!my-new-command [arguments]

        Longer description with examples and details.
        """
        # Parse arguments if needed
        if not args:
            await ctx.reply("Please provide arguments!", mention_author=False)
            return

        try:
            # Your command logic here
            result = await bot.comfy.some_method(args)

            # Send response
            await ctx.reply(f"Success! Result: {result}", mention_author=False)

        except Exception as exc:
            logger.exception("Failed to execute my-new-command")
            await ctx.reply(
                f"An error occurred: {type(exc).__name__}: {exc}",
                mention_author=False,
            )

Step 3: Import Required Dependencies

At the top of your command module, import what you need:

import logging
from discord.ext import commands
from discord_utils import require_comfy_client, parse_labeled_args
from config import ARG_PROMPT_KEY, ARG_TYPE_KEY
from job_queue import Job

logger = logging.getLogger(__name__)

Step 4: Test Your Command

Run the bot and test your command:

python bot.py

Then in Discord:

ttr!my-new-command test arguments

Adding a New Feature Module

If your commands don't fit existing modules, create a new one:

Step 1: Create the Module File

Create commands/your_feature.py:

"""
commands/your_feature.py
========================

Description of what this module handles.
"""

from __future__ import annotations

import logging
from discord.ext import commands
from discord_utils import require_comfy_client

logger = logging.getLogger(__name__)


def setup_your_feature_commands(bot, config):
    """
    Register your feature commands with the bot.

    Parameters
    ----------
    bot : commands.Bot
        The Discord bot instance.
    config : BotConfig
        The bot configuration object.
    """

    @bot.command(name="feature-command")
    @require_comfy_client
    async def feature_command(ctx: commands.Context, *, args: str = "") -> None:
        """Command description."""
        await ctx.reply("Feature command executed!", mention_author=False)

    @bot.command(name="another-command", aliases=["ac"])
    async def another_command(ctx: commands.Context) -> None:
        """Another command description."""
        await ctx.reply("Another command!", mention_author=False)

Step 2: Register in commands/init.py

Edit commands/__init__.py to import and register your module:

from .generation import setup_generation_commands
from .workflow import setup_workflow_commands
from .upload import setup_upload_commands
from .history import setup_history_commands
from .workflow_changes import setup_workflow_changes_commands
from .your_feature import setup_your_feature_commands  # ADD THIS


def register_all_commands(bot, config):
    """Register all bot commands."""
    setup_generation_commands(bot, config)
    setup_workflow_commands(bot)
    setup_upload_commands(bot)
    setup_history_commands(bot)
    setup_workflow_changes_commands(bot)
    setup_your_feature_commands(bot, config)  # ADD THIS

Step 3: Update Documentation

Add your module to CLAUDE.md:

### File Structure

commands/ ├── init.py ├── generation.py ├── workflow.py ├── upload.py ├── history.py ├── workflow_changes.py └── your_feature.py # Your new module


Adding Configuration Options

Configuration is centralized in config.py. Here's how to add new options:

Step 1: Add Constants (if needed)

Edit config.py and add constants in the appropriate section:

# ========================================
# Your Feature Constants
# ========================================

MY_FEATURE_DEFAULT_VALUE = 42
"""Default value for my feature."""

MY_FEATURE_MAX_LIMIT = 100
"""Maximum limit for my feature."""

Step 2: Add to BotConfig (if environment variable)

If your config comes from environment variables, add it to BotConfig:

@dataclass
class BotConfig:
    """Configuration container for the Discord ComfyUI bot."""

    discord_bot_token: str
    comfy_server: str
    comfy_output_path: str
    comfy_history_limit: int
    workflow_file: Optional[str] = None
    my_feature_enabled: bool = False  # ADD THIS
    my_feature_value: int = MY_FEATURE_DEFAULT_VALUE  # ADD THIS

Step 3: Load in from_env()

Add loading logic in BotConfig.from_env():

@classmethod
def from_env(cls) -> BotConfig:
    """Create a BotConfig instance by loading from environment."""
    # ... existing code ...

    # Load your feature config
    my_feature_enabled = os.getenv("MY_FEATURE_ENABLED", "false").lower() == "true"

    try:
        my_feature_value = int(os.getenv("MY_FEATURE_VALUE", str(MY_FEATURE_DEFAULT_VALUE)))
    except ValueError:
        my_feature_value = MY_FEATURE_DEFAULT_VALUE

    return cls(
        # ... existing parameters ...
        my_feature_enabled=my_feature_enabled,
        my_feature_value=my_feature_value,
    )

Step 4: Use in Your Commands

Access config in your commands:

def setup_your_feature_commands(bot, config):
    @bot.command(name="feature")
    async def feature_command(ctx: commands.Context):
        if not config.my_feature_enabled:
            await ctx.reply("Feature is disabled!", mention_author=False)
            return

        value = config.my_feature_value
        await ctx.reply(f"Feature value: {value}", mention_author=False)

Step 5: Document the Environment Variable

Update CLAUDE.md and add to .env.example (if you create one):

# Feature Configuration
MY_FEATURE_ENABLED=true
MY_FEATURE_VALUE=42

Working with Workflows

The bot has separate concerns for workflows:

  • WorkflowManager (workflow_manager.py) - Template storage and node manipulation
  • WorkflowStateManager (workflow_state.py) - Runtime state (prompt, negative_prompt, input_image)
  • ComfyClient (comfy_client.py) - Uses both managers to generate images

Adding New Workflow Node Types

If you need to manipulate new types of nodes in workflows:

Step 1: Add Method to WorkflowManager

Edit workflow_manager.py:

def find_and_replace_my_node(
    self, workflow: Dict[str, Any], my_value: str
) -> Dict[str, Any]:
    """
    Find and replace my custom node type.

    This searches for nodes of a specific class_type and updates their inputs.

    Parameters
    ----------
    workflow : Dict[str, Any]
        The workflow definition to modify.
    my_value : str
        The value to inject.

    Returns
    -------
    Dict[str, Any]
        The modified workflow.
    """
    for node_id, node in workflow.items():
        if node.get("class_type") == "MyCustomNodeType" and node.get("inputs"):
            # Check metadata for specific node identification
            meta = node.get("_meta", {})
            if "My Custom Node" in meta.get("title", ""):
                workflow[node_id]["inputs"]["my_input"] = my_value
                logger.debug("Replaced my_value in node %s", node_id)

    return workflow

Step 2: Add to apply_state_changes()

Update apply_state_changes() to include your new manipulation:

def apply_state_changes(
    self,
    workflow: Dict[str, Any],
    prompt: Optional[str] = None,
    negative_prompt: Optional[str] = None,
    input_image: Optional[str] = None,
    my_custom_value: Optional[str] = None,  # ADD THIS
    randomize_seed: bool = True,
) -> Dict[str, Any]:
    """Apply multiple state changes to a workflow in one pass."""
    if randomize_seed:
        workflow = self.find_and_replace_seed(workflow)

    if prompt is not None:
        workflow = self.find_and_replace_prompt(workflow, prompt)

    if negative_prompt is not None:
        workflow = self.find_and_replace_negative_prompt(workflow, negative_prompt)

    if input_image is not None:
        workflow = self.find_and_replace_input_image(workflow, input_image)

    # ADD THIS
    if my_custom_value is not None:
        workflow = self.find_and_replace_my_node(workflow, my_custom_value)

    return workflow

Step 3: Add State to WorkflowStateManager

Edit workflow_state.py to track the new state:

def __init__(self, state_file: Optional[str] = None):
    """Initialize the workflow state manager."""
    self._state: Dict[str, Any] = {
        "prompt": None,
        "negative_prompt": None,
        "input_image": None,
        "my_custom_value": None,  # ADD THIS
    }
    # ... rest of init ...

def set_my_custom_value(self, value: str) -> None:
    """Set the custom value."""
    self._state["my_custom_value"] = value
    if self._state_file:
        try:
            self.save_to_file()
        except Exception:
            pass

def get_my_custom_value(self) -> Optional[str]:
    """Get the custom value."""
    return self._state.get("my_custom_value")

Step 4: Use in ComfyClient

The ComfyClient will automatically use your new state if you update generate_image_with_workflow():

async def generate_image_with_workflow(self) -> tuple[List[bytes], List[dict[str, Any]], str]:
    # ... existing code ...

    # Get current state changes
    changes = self.state_manager.get_changes()
    prompt = changes.get("prompt")
    negative_prompt = changes.get("negative_prompt")
    input_image = changes.get("input_image")
    my_custom_value = changes.get("my_custom_value")  # ADD THIS

    # Apply changes using WorkflowManager
    workflow = self.workflow_manager.apply_state_changes(
        workflow,
        prompt=prompt,
        negative_prompt=negative_prompt,
        input_image=input_image,
        my_custom_value=my_custom_value,  # ADD THIS
        randomize_seed=True,
    )

    # ... rest of method ...

Best Practices

Command Design

  1. Use descriptive names: ttr!generate is better than ttr!gen (but provide aliases)
  2. Validate inputs early: Check arguments before starting long operations
  3. Provide clear feedback: Tell users what's happening and when it's done
  4. Handle errors gracefully: Catch exceptions and show user-friendly messages
  5. Use decorators: @require_comfy_client eliminates boilerplate

Code Organization

  1. One responsibility per module: Don't mix unrelated commands
  2. Keep functions small: If a function is > 50 lines, consider splitting it
  3. Use type hints: Help future developers understand your code
  4. Document with docstrings: Explain what, why, and how

Discord Best Practices

  1. Use mention_author=False: Prevents spam from @mentions
  2. Use delete_after=X: For temporary status messages
  3. Use ephemeral=True: For interaction responses (buttons/modals)
  4. Limit file attachments: Discord has a 4-file limit (use MAX_IMAGES_PER_RESPONSE)

Performance

  1. Use job queue for long operations: Queue generation requests
  2. Use typing indicator: async with ctx.typing(): shows bot is working
  3. Batch operations: Don't send 10 separate messages when 1 will do
  4. Close resources: Always close aiohttp sessions, file handles

Common Patterns

Pattern 1: Labeled Argument Parsing

For commands with key:value syntax:

from discord_utils import parse_labeled_args
from config import ARG_PROMPT_KEY, ARG_TYPE_KEY

@bot.command(name="my-command")
async def my_command(ctx: commands.Context, *, args: str = ""):
    # Parse labeled arguments
    parsed = parse_labeled_args(args, [ARG_PROMPT_KEY, ARG_TYPE_KEY])

    prompt = parsed.get("prompt")  # None if not provided
    image_type = parsed.get("type") or "input"  # Default to "input"

    if not prompt:
        await ctx.reply("Please provide a prompt!", mention_author=False)
        return

Pattern 2: Queued Job Execution

For long-running operations:

from job_queue import Job

@bot.command(name="long-operation")
@require_comfy_client
async def long_operation(ctx: commands.Context, *, args: str = ""):
    try:
        # Define the job function
        async def _run_job():
            async with ctx.typing():
                result = await bot.comfy.some_long_operation(args)
                await ctx.reply(f"Done! Result: {result}", mention_author=False)

        # Submit to queue
        position = await bot.jobq.submit(
            Job(
                label=f"long-operation:{ctx.author.id}",
                run=_run_job,
            )
        )

        await ctx.reply(
            f"Queued ✅ (position: {position})",
            mention_author=False,
            delete_after=2.0,
        )

    except Exception as exc:
        logger.exception("Failed to queue long operation")
        await ctx.reply(
            f"An error occurred: {type(exc).__name__}: {exc}",
            mention_author=False,
        )

Pattern 3: File Attachments

For uploading files to ComfyUI:

@bot.command(name="upload-and-process")
@require_comfy_client
async def upload_and_process(ctx: commands.Context):
    if not ctx.message.attachments:
        await ctx.reply("Please attach a file!", mention_author=False)
        return

    for attachment in ctx.message.attachments:
        try:
            # Download attachment
            data = await attachment.read()

            # Upload to ComfyUI
            result = await bot.comfy.upload_image(
                data,
                attachment.filename,
                image_type="input",
            )

            # Process the uploaded file
            filename = result.get("name")
            await ctx.reply(f"Uploaded: {filename}", mention_author=False)

        except Exception as exc:
            logger.exception("Failed to process attachment")
            await ctx.reply(
                f"Failed: {attachment.filename}: {exc}",
                mention_author=False,
            )

Pattern 4: Interactive UI (Buttons)

For adding buttons to messages:

from discord.ui import View, Button
import discord

class MyView(View):
    def __init__(self, data: str):
        super().__init__(timeout=None)
        self.data = data

    @discord.ui.button(label="Click Me", style=discord.ButtonStyle.primary)
    async def button_callback(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ):
        # Handle button click
        await interaction.response.send_message(
            f"You clicked! Data: {self.data}",
            ephemeral=True,
        )

@bot.command(name="interactive")
async def interactive(ctx: commands.Context):
    view = MyView(data="example")
    await ctx.reply("Click the button:", view=view, mention_author=False)

Pattern 5: Using Configuration

Access bot configuration in commands:

def setup_my_commands(bot, config):
    @bot.command(name="check-config")
    async def check_config(ctx: commands.Context):
        # Access config values
        server = config.comfy_server
        output_path = config.comfy_output_path

        await ctx.reply(
            f"Server: {server}\nOutput: {output_path}",
            mention_author=False,
        )

Testing Your Changes

Manual Testing Checklist

  1. Start the bot: python bot.py

    • Verify no import errors
    • Check configuration loads correctly
    • Confirm all commands register
  2. Test basic functionality:

    ttr!test
    ttr!help
    ttr!your-new-command
    
  3. Test error handling:

    • Run command with missing arguments
    • Run command with invalid arguments
    • Test when ComfyUI is unavailable (if applicable)
  4. Test edge cases:

    • Very long inputs
    • Special characters
    • Concurrent command execution
    • Commands while queue is full

Syntax Validation

Check for syntax errors without running:

python -m py_compile bot.py
python -m py_compile commands/your_feature.py

Check Imports

Verify all imports work:

python -c "from commands.your_feature import setup_your_feature_commands; print('OK')"

Code Style

Follow these conventions:

  • Indentation: 4 spaces (no tabs)
  • Line length: Max 100 characters (documentation can be longer)
  • Docstrings: Use Google style or NumPy style (match existing code)
  • Imports: Group stdlib, third-party, local (separated by blank lines)
  • Type hints: Use modern syntax (dict[str, Any] not Dict[str, Any])

Example: Complete Feature Addition

Here's a complete example adding a "status" command:

1. Create commands/status.py

"""
commands/status.py
==================

Bot status and diagnostics commands.
"""

from __future__ import annotations

import logging
import asyncio
from discord.ext import commands
from discord_utils import require_comfy_client

logger = logging.getLogger(__name__)


def setup_status_commands(bot, config):
    """Register status commands with the bot."""

    @bot.command(name="status", aliases=["s", "stat"])
    async def status_command(ctx: commands.Context) -> None:
        """
        Show bot status and queue information.

        Usage:
            ttr!status

        Displays:
        - Bot connection status
        - ComfyUI connection status
        - Current queue size
        - Configuration info
        """
        # Check bot status
        latency_ms = round(bot.latency * 1000)

        # Check queue
        if hasattr(bot, "jobq"):
            queue_size = await bot.jobq.get_queue_size()
        else:
            queue_size = 0

        # Check ComfyUI
        comfy_status = "✅ Connected" if hasattr(bot, "comfy") else "❌ Not configured"

        # Build status message
        status_msg = [
            "**Bot Status**",
            f"• Latency: {latency_ms}ms",
            f"• Queue size: {queue_size}",
            f"• ComfyUI: {comfy_status}",
            f"• Server: {config.comfy_server}",
        ]

        await ctx.reply("\n".join(status_msg), mention_author=False)

    @bot.command(name="ping")
    async def ping_command(ctx: commands.Context) -> None:
        """
        Check bot responsiveness.

        Usage:
            ttr!ping
        """
        latency_ms = round(bot.latency * 1000)
        await ctx.reply(f"🏓 Pong! Latency: {latency_ms}ms", mention_author=False)

2. Register in commands/init.py

from .status import setup_status_commands

def register_all_commands(bot, config):
    # ... existing registrations ...
    setup_status_commands(bot, config)

3. Update CLAUDE.md

- **commands/status.py** - Bot status and diagnostics commands

4. Test

python bot.py

In Discord:

ttr!status
ttr!ping
ttr!s  (alias test)

Getting Help

If you're stuck:

  1. Check CLAUDE.md: Architecture and patterns documented there
  2. Read existing commands: See how similar features are implemented
  3. Check logs: Run bot and check console output for errors
  4. Test incrementally: Add small pieces and test frequently

Remember: The refactored architecture makes adding features straightforward. Follow the patterns, and your code will fit right in! 🚀