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:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
112
backend/app/twitch_service.py
Normal file
112
backend/app/twitch_service.py
Normal 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}")
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user