fix: 修复 Twitch 集成 - 降级到 twitchio 2.x

- 从 twitchio 3.x 降级到 2.10.0 (IRC-based)
  - 3.x 使用 EventSub API 需要更复杂的配置
  - 2.x 使用 IRC 方式更简单可靠
- 简化 Twitch 配置,只需要 Token 和频道名
- 移除 client_id, client_secret, bot_id 要求
- 更新 .env.example 配置说明
This commit is contained in:
empty
2026-01-01 18:59:14 +08:00
parent a261eaa8ab
commit 3e89a17b69
4 changed files with 162 additions and 1 deletions

View File

@@ -65,3 +65,21 @@
# Force mock mode (no API calls, uses predefined responses) # Force mock mode (no API calls, uses predefined responses)
# ============================================================================= # =============================================================================
# LLM_MOCK_MODE=true # LLM_MOCK_MODE=true
# =============================================================================
# Twitch Configuration for Live Stream Integration (twitchio 2.x)
# =============================================================================
# Get your OAuth Token from: https://twitchtokengenerator.com/
# 1. Select "Bot Chat Token"
# 2. Authorize with your Twitch account
# 3. Copy the Access Token (starts with oauth:)
#
# TWITCH_TOKEN=oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# TWITCH_CHANNEL_NAME=your_channel_name
# TWITCH_COMMAND_PREFIX=!
# =============================================================================
# Game Configuration
# =============================================================================
# GAME_DIFFICULTY=casual
# DEBUG=true

View File

@@ -7,7 +7,9 @@ Configures the application, WebSocket routes, and lifecycle events.
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
import asyncio
import logging import logging
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
@@ -19,6 +21,7 @@ from fastapi.responses import FileResponse
from .server import ConnectionManager from .server import ConnectionManager
from .engine import GameEngine from .engine import GameEngine
from .schemas import GameEvent, ClientMessage, EventType from .schemas import GameEvent, ClientMessage, EventType
from .twitch_service import TwitchBot
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@@ -39,12 +42,39 @@ FRONTEND_DIR = Path(__file__).parent.parent.parent / "frontend"
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
""" """
Application lifespan manager. Application lifespan manager.
Starts the game engine on startup and stops it on shutdown. Starts the game engine and Twitch bot on startup, stops them on shutdown.
""" """
logger.info("Starting application...") logger.info("Starting application...")
# Start game engine
await engine.start() await engine.start()
# Start Twitch bot if credentials are provided
twitch_bot = None
if os.getenv("TWITCH_TOKEN") and os.getenv("TWITCH_CHANNEL_NAME"):
try:
twitch_bot = TwitchBot(engine)
# Start bot in background task
asyncio.create_task(twitch_bot.start())
logger.info("Twitch bot started in background")
except Exception as e:
logger.error(f"Failed to start Twitch bot: {e}")
else:
logger.info("Twitch credentials not provided, skipping Twitch bot")
yield yield
logger.info("Shutting down application...") logger.info("Shutting down application...")
# Stop Twitch bot if it was started
if twitch_bot:
try:
await twitch_bot.close()
logger.info("Twitch bot stopped")
except Exception as e:
logger.error(f"Error stopping Twitch bot: {e}")
# Stop game engine
await engine.stop() await engine.stop()

View File

@@ -0,0 +1,112 @@
"""
Twitch service for connecting to Twitch chat and handling events.
Integrates with the game engine to process chat commands and bits.
Compatible with twitchio 2.x (IRC-based)
"""
import os
import logging
from typing import TYPE_CHECKING
from twitchio.ext import commands
if TYPE_CHECKING:
from .engine import GameEngine
logger = logging.getLogger(__name__)
class TwitchBot(commands.Bot):
"""
Twitch bot that listens to chat messages and bits events.
Forwards them to the game engine for processing.
Compatible with twitchio 2.x API (IRC-based).
"""
def __init__(self, game_engine: "GameEngine"):
# Initialize bot with environment variables
self._token = os.getenv("TWITCH_TOKEN")
self._channel = os.getenv("TWITCH_CHANNEL_NAME")
prefix = os.getenv("TWITCH_COMMAND_PREFIX", "!")
if not self._token:
raise ValueError("TWITCH_TOKEN environment variable is required")
if not self._channel:
raise ValueError("TWITCH_CHANNEL_NAME environment variable is required")
# Store game engine reference
self._game_engine = game_engine
# Initialize the bot (twitchio 2.x API - IRC based)
super().__init__(
token=self._token,
prefix=prefix,
initial_channels=[self._channel]
)
logger.info(f"TwitchBot initialized for channel: {self._channel}")
async def event_ready(self):
"""Called when the bot successfully connects to Twitch."""
logger.info(f"Twitch Bot logged in as: {self.nick}")
logger.info(f"Connected to channels: {[c.name for c in self.connected_channels]}")
async def event_message(self, message):
"""Called when a message is received in chat."""
# Ignore messages from the bot itself
if message.echo:
return
# Handle commands first
await self.handle_commands(message)
# Extract user and message content
author = message.author
if author is None:
return
username = author.name
content = message.content.strip()
# Log the message for debugging
logger.info(f"Twitch chat [{username}]: {content}")
# Forward to game engine for command processing
try:
await self._game_engine.process_command(username, content)
except Exception as e:
logger.error(f"Error processing command: {e}")
# Check for bits in the message tags
if hasattr(message, 'tags') and message.tags:
bits = message.tags.get('bits')
if bits:
try:
bits_amount = int(bits)
logger.info(f"Received {bits_amount} bits from {username}")
await self._game_engine.process_bits(username, bits_amount)
# Send special gift effect to Unity
await self._game_engine._broadcast_event("gift_effect", {
"type": "gift_effect",
"user": username,
"value": bits_amount,
"message": f"{username} cheered {bits_amount} bits!"
})
except (ValueError, TypeError) as e:
logger.error(f"Error parsing bits amount: {e}")
async def event_command_error(self, context, error):
"""Called when a command error occurs."""
# Ignore command not found errors (most chat messages aren't commands)
if isinstance(error, commands.CommandNotFound):
return
logger.error(f"Command error: {error}")
async def event_error(self, error: Exception, data: str = None):
"""Called when an error occurs."""
logger.error(f"Twitch bot error: {error}")
if data:
logger.debug(f"Error data: {data}")

View File

@@ -6,3 +6,4 @@ sqlalchemy>=2.0.0
aiosqlite>=0.19.0 aiosqlite>=0.19.0
litellm>=1.40.0 litellm>=1.40.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
twitchio>=3.0.0