You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Discord Bot命令修改后无法同步的技术求助

问题描述

我的Discord Bot已稳定运行10个月,此前所有带选项的命令同步均正常。昨日参照Apex命令的实现方式,为Fortnite命令添加选项后,本地启动Bot时控制台显示“Commands Synced”,但刷新Discord后命令未更新,新增测试命令也不显示。Bot的状态更新功能正常,已尝试本地/部署运行、重启刷新Discord等操作,期望Fortnite命令能像Apex命令一样显示对应选项。


相关代码

Fortnite.py

import discord
from discord.ext import commands
import datetime
import requests
from typing import Literal
import os

TIME_MAPPING = {
    'Season': 'season',
    'Lifetime': 'lifetime',
}


async def fortnite(interaction: discord.Interaction, time: Literal['Season', 'Lifetime'],
                   name: str = None):
    try:
        if name is None:
            raise ValueError("Please provide a username.")

        if time is None:
            raise ValueError("Please provide a platform (Season, Lifetime).")

        time_window = TIME_MAPPING.get(time)
        if not api_platform:
            raise ValueError("Invalid time range. Please use Season or Lifetime")

        response = requests.get(
            f"https://fortnite-api.com/v2/stats/br/v2?timeWindow={time_window}&name={name}",
            headers={"Authorization": os.getenv('FORTNITE_API_KEY')}
        )

        response.raise_for_status()
        data = response.json()

        if 'data' not in data:
            raise ValueError("Invalid data structure in API response.")

        stats = data['data']
        account = stats['account']
        battle_pass = stats['battlePass']

        # Correct win rate calculation
        wins = stats['stats']['all']['overall']['wins']
        matches = stats['stats']['all']['overall']['matches']
        if matches > 0:
            calculated_win_rate = wins / matches
        else:
            calculated_win_rate = 0

        embed = discord.Embed(title=f"Fortnite - {name}", color=0xdd4f7a,
                              url=f"https://fortnitetracker.com/profile/all/{name}")
        embed.set_thumbnail(url="https://seeklogo.com/images/F/fortnite-logo-1F7897BD1E-seeklogo.com.png")
        embed.add_field(name="Account", value=f"Name: {account['name']}\nLevel: {battle_pass['level']}", inline=True)
        embed.add_field(name="Match Placements",
                        value=f"Victory Royales: {wins} \nTop 5: {stats['stats']['all']['overall']['top5']}\nTop 12: {stats['stats']['all']['overall']['top12']}",
                        inline=True)

        # Additional Stats
        embed.add_field(name="Kill Stats",
                        value=f"Kills/Deaths: {stats['stats']['all']['overall']['kills']:,}/{stats['stats']['all']['overall']['deaths']:,}\n"
                              f"KD Ratio: {stats['stats']['all']['overall']['kd']:.2f}\n"
                              f"Kills Per Minute: {stats['stats']['all']['overall']['killsPerMin']:.2f}\n"
                              f"Kills Per Match: {stats['stats']['all']['overall']['killsPerMatch']:.2f}\n"
                              f"Players Outlived: {stats['stats']['all']['overall']['playersOutlived']:,}",
                        inline=False)

        embed.add_field(name="Match Stats",
                        value=f"Total Matches Played: {matches:,}\n"
                              f"Win Rate: {calculated_win_rate:.2%}\n"
                              f"Total Score: {stats['stats']['all']['overall']['score']:,}\n"
                              f"Score Per Minute: {stats['stats']['all']['overall']['scorePerMin']:.0f}\n"
                              f"Score Per Match: {stats['stats']['all']['overall']['scorePerMatch']:.0f}\n"
                              f"Total Minutes Played: {stats['stats']['all']['overall']['minutesPlayed']:,}",
                        inline=True)

        embed.timestamp = datetime.datetime.now(datetime.UTC)
        embed.set_footer(text="Built By Goldiez ❤️")
        await interaction.response.send_message(embed=embed)

    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")
        await interaction.response.send_message(
            "Sorry, I couldn't retrieve Fortnite stats at the moment. Please try again later.")

    except (KeyError, ValueError) as e:
        print(f"Error: {e}")
        await interaction.response.send_message(
            "Failed to retrieve Fortnite stats. The Fortnite API is Currently Unavailable")

    except Exception as e:
        print(f"Error: {e}")
        await interaction.response.send_message(
            "Oops! An unexpected error occurred while processing your request. Please try again later.")


def setup(client):
    client.tree.command(
        name="fortnite", description="Check your Fortnite Player Stats"
    )(fortnite)

Apex.py

import discord
import datetime
import requests
from typing import Literal
import os

PLATFORM_MAPPING = {
    'Xbox': 'xbl',
    'Playstation': 'psn',
    'Origin (PC)': 'origin',
}


async def apex(interaction: discord.Interaction, platform: Literal['Xbox', 'Playstation', 'Origin (PC)'],
               name: str = None):
    try:
        if name is None:
            raise ValueError("Please provide a username.")

        if platform is None:
            raise ValueError("Please provide a platform (Xbox, Playstation, Origin).")

        api_platform = PLATFORM_MAPPING.get(platform)
        if not api_platform:
            raise ValueError("Invalid platform. Please use Xbox, Playstation, or Origin.")

        response = requests.get(f"https://public-api.tracker.gg/v2/apex/standard/profile/{api_platform}/{name}",
                                headers={"TRN-Api-Key": os.getenv('TRN-Api-Key')})

        response.raise_for_status()

        data = response.json()

        if 'data' not in data or 'segments' not in data['data']:
            error_message = f"Invalid data structure in API response. Response: {data}"
            print(f"Error: {error_message}")
            raise ValueError(error_message)

        segments = data['data']['segments']

        if not segments:
            await interaction.response.send_message(f"No segments found for the given username: {name}")
            return

        lifetime = segments[0]['stats']
        ranked = segments[0]['stats']['rankScore']
        peak_rank = segments[0]['stats']['lifetimePeakRankScore']

        active_legend_name = data['data'].get('metadata', {}).get('activeLegendName', 'Unknown')
        active_legend_data = next(
            (legend for legend in segments if legend['metadata']['name'] == active_legend_name), None)

        # Helper function to get the percentile label
        def get_percentile_label(percentile):
            if percentile is not None:
                if percentile >= 90:
                    return '🌟 Top'
                else:
                    return 'Top' if percentile >= 50 else 'Bottom'
            else:
                return 'N/A'

        def format_stat_value(stat_data):
            stat_value = stat_data.get('value')
            if stat_value is not None:
                percentile_label = get_percentile_label(stat_data.get('percentile', 0))
                percentile_value = int(stat_data.get('percentile', 0)) if percentile_label != 'N/A' else 0
                return f"{int(stat_value):,} ({percentile_label} {percentile_value}%)"
            else:
                return 'N/A'

        legend_color = active_legend_data.get('metadata', {}).get('legendColor', '#9B8651')
        # Create the embed at the beginning
        embed = discord.Embed(title=f"Apex Legends - {name}",
                              url=f"https://apex.tracker.gg/apex/profile/{api_platform}/{name}/overview",
                              color=int(legend_color[1:], 16))

        # Main lifetime stats
        level_data = lifetime.get('level', {})
        kills_data = lifetime.get('kills', {})
        damage_data = lifetime.get('damage', {})
        matches_played_data = lifetime.get('matchesPlayed', {})
        arena_winstreak_data = lifetime.get('arenaWinStreak', {})

        formatted_level = format_stat_value(level_data)
        formatted_kills = format_stat_value(kills_data)
        formatted_damage = format_stat_value(damage_data)
        formatted_matches_played = format_stat_value(matches_played_data)
        formatted_arena_winstreak = format_stat_value(arena_winstreak_data)

        # Legend stats
        if active_legend_data and 'stats' in active_legend_data:
            # Embed setup
            embed.set_thumbnail(url=f"{active_legend_data['metadata']['portraitImageUrl']}")

            # Add fields to the embed only if the corresponding stats are available
            embed.add_field(name="Lifetime", value=f"Level: **{formatted_level}**"
                                                   f"\nKills: **{formatted_kills}**"
                                                   f"\nDamage: **{formatted_damage}**"
                                                   f"\nMatches Played: **{formatted_matches_played}**"
                                                   f"\nArena Winstreak: **{formatted_arena_winstreak}**", inline=True)

            # Add the dynamic fields to the embed
            embed_fields = []
            for stat_key, stat_data in active_legend_data['stats'].items():
                stat_name = stat_data.get('displayName', stat_key.capitalize())
                stat_value = int(stat_data.get('value', 0)) if stat_data.get('value') is not None else 0
                stat_percentile = stat_data.get('percentile', 0)

                field_str = f"{stat_name}: **{stat_value} ({get_percentile_label(stat_percentile)} {int(stat_percentile) if stat_percentile is not None else 0}%)**"
                embed_fields.append(field_str)

            # Add other fields to the embed
            embed.add_field(name="Current Rank",
                            value=f"**_Battle Royale Rank_**\n{ranked.get('metadata', {}).get('rankName', 0)}: **{int(ranked.get('value', 0)):,}**"
                                  f"\n# {int(ranked.get('rank', 0) or 0):,} • {int(ranked.get('percentile', 0) or 0)}%",
                            inline=True)

            embed.add_field(name="Peak Rank",
                            value=f"**_Battle Royale Rank_**\n{peak_rank.get('metadata', {}).get('rankName', 0)}: **{int(peak_rank.get('value', 0)):,}**",
                            inline=False)

            # Add the dynamic fields to the embed
            embed.add_field(name=f"{active_legend_name} - Currently Selected", value='\n'.join(embed_fields),
                            inline=False)

            embed.timestamp = datetime.datetime.now(datetime.UTC)
            embed.set_footer(text="Built By Goldiez ❤️")
            await interaction.response.send_message(embed=embed)

            if not interaction.response.is_done():
                await interaction.response.send_message(embed=embed)

    except ValueError as e:
        print(f"Error: {e}")
        if not interaction.response.is_done():
            await interaction.response.send_message(f"Oops! Something went wrong. {e}")

    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")
        if not interaction.response.is_done():
            await interaction.response.send_message(
                "Sorry, I couldn't retrieve Apex Legends stats at the moment. Please try again later.")

    except Exception as e:
        print(f"Error: {e}")
        if not interaction.response.is_done():
            await interaction.response.send_message(
                "Oops! An unexpected error occurred while processing your request. Please try again later.")


def setup(client):
    client.tree.command(
        name="apex", description="Check your Apex Player Stats"
    )(apex)

bot.py

import discord
from discord.ext import commands
import os
from dotenv import load_dotenv
import asyncio
import logging
from commands import apex, league, fortnite, horoscope, help, review, tft, servers, kick
import re

# Load environment variables
load_dotenv()

# Get the blacklisted guild IDs from the environment variable
blacklisted_guilds = set(map(int, os.getenv('BLACKLISTED_GUILDS', None).split(','))) if os.getenv(
    'BLACKLISTED_GUILDS') else set()

logger = logging.getLogger('discord.gateway')
logger.setLevel(logging.ERROR)  # Maybe fix as server grows

# Create the bot instance
intents = discord.Intents.all()
client = commands.Bot(command_prefix="/", help_command=None, intents=intents)

# Setup command modules
apex.setup(client)
league.setup(client)
fortnite.setup(client)
horoscope.setup(client)
tft.setup(client)
# servers.setup(client)
# kick.setup(client)
help.setup(client)
review.setup(client)

# Regex pattern to match Discord invite links
invite_link_pattern = re.compile(r"(https?://)?(www\.)?(discord\.gg|discordapp\.com/invite)/[A-Za-z0-9]+")


# Event for when the bot is ready
@client.event
async def on_ready():
    print(f'{client.user} has connected to Discord!')

    try:
        await client.wait_until_ready()  # Wait until the bot is fully connected
        print(f"Commands Synced")
    except Exception as e:
        print(e)

    await update_presence()  # Call the function to set initial presence


# Function to update the bot's presence
async def update_presence():
    while True:
        guild_count = len(client.guilds)
        presence = discord.Activity(
            type=discord.ActivityType.playing, name=f"on {guild_count} servers")
        await client.change_presence(activity=presence)
        await asyncio.sleep(18000)  # Update every 5 hours


@client.event
async def on_message(message):
    if message.author == client.user:
        return

    # Check if the message contains a Discord invite link
    if invite_link_pattern.search(message.content):
        await message.channel.send(
            "Need Horoscopes? Apex Stats? Fortnite Stats? LoL Stats? Idk Add me! <https://astrostats.vercel.app>")

    await client.process_commands(message)


# Event for handling interaction errors
@client.event
async def p_error(interaction: discord.Interaction, error):
    if isinstance(error, commands.MissingRequiredArguments):
        await interaction.response.send_message('Missing required arguments.')


# Event for checking if the guild is blacklisted before joining
@client.event
async def on_guild_join(guild):
    if guild.id in blacklisted_guilds:
        await guild.leave()
        print(f"Left blacklisted guild: {guild.name} ({guild.id})")


# Start the bot
client.run(os.getenv('TOKEN'))

解决方案

1. 修复命令同步逻辑

你的bot.py中on_ready事件仅打印了同步提示,未执行实际的命令同步操作。Discord要求显式调用client.tree.sync()才能将slash命令同步到服务器。修改on_ready事件:

@client.event
async def on_ready():
    print(f'{client.user} has connected to Discord!')

    try:
        await client.wait_until_ready()
        # 执行命令同步并打印同步数量
        synced = await client.tree.sync()
        print(f"Synced {len(synced)} command(s)")
    except Exception as e:
        print(e)

    await update_presence()
  • 全局命令同步后可能需要1小时左右生效,测试阶段可指定服务器ID快速同步:
    # 替换为你的测试服务器ID
    TEST_GUILD = discord.Object(id=1234567890)
    synced = await client.tree.sync(guild=TEST_GUILD)
    

2. 修复Fortnite.py中的变量错误

在fortnite函数中,存在未定义变量的错误:

# 错误代码
if not api_platform:
    raise ValueError("Invalid time range. Please use Season or Lifetime")

此处api_platform未定义,应替换为实际使用的time_window变量:

# 修正后
if not time_window:
    raise ValueError("Invalid time range. Please use Season or Lifetime")

该错误会导致命令执行时抛出异常,也可能影响命令注册流程,必须修复。

3. 规范命令参数的必填性

虽然函数内判断了name和time是否为空,但Discord slash命令参数默认必填。如果name是必填项,可直接去掉默认值,让Discord在调用时强制要求用户输入,省去函数内的判断:

async def fortnite(interaction: discord.Interaction, time: Literal['Season', '
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 00:00:08