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

781 lines
20 KiB
Markdown

# 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](#adding-a-new-command)
- [Adding a New Feature Module](#adding-a-new-feature-module)
- [Adding Configuration Options](#adding-configuration-options)
- [Working with Workflows](#working-with-workflows)
- [Best Practices](#best-practices)
- [Common Patterns](#common-patterns)
- [Testing Your Changes](#testing-your-changes)
---
## 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](#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:
```python
# 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:
```python
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:
```bash
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`:
```python
"""
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:
```python
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`:
```markdown
### 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:
```python
# ========================================
# 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`:
```python
@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()`:
```python
@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:
```python
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):
```bash
# 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`:
```python
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:
```python
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:
```python
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()`:
```python
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:
```python
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:
```python
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:
```python
@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:
```python
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:
```python
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:
```bash
python -m py_compile bot.py
python -m py_compile commands/your_feature.py
```
### Check Imports
Verify all imports work:
```bash
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
```python
"""
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
```python
from .status import setup_status_commands
def register_all_commands(bot, config):
# ... existing registrations ...
setup_status_commands(bot, config)
```
### 3. Update CLAUDE.md
```markdown
- **commands/status.py** - Bot status and diagnostics commands
```
### 4. Test
```bash
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! 🚀