返回提交历史
Modified
projects/discord-bot/bot.py
+113
-2
Modified
projects/discord-bot/live_feed.py
+113
-0
XFEstudio/gpt4free
Update discord bot
46238695
代码差异
2 个文件
+226
-2
@@ -23,6 +23,7 @@ import logging
23
23
from collections import defaultdict, deque
24
24
from typing import Deque, Dict, List, Optional
25
25
26
import aiohttp
26
27
import discord
27
28
from discord import app_commands
28
29
from discord.ext import commands
@@ -30,6 +31,7 @@ from dotenv import load_dotenv
30
31
31
32
from g4f.providers.any_provider import AnyProvider
32
33
from g4f.client import ClientFactory
34
from g4f.Provider import G4FSpace
33
35
34
36
from mcp_tools import MCPToolManager, ALL_AVAILABLE_TOOLS, SAFE_DEFAULT_TOOLS
35
37
from live_feed import LiveFeed
@@ -65,7 +67,8 @@ PUBLIC_BASE = os.getenv("G4F_PUBLIC_BASE", API_BASE)
65
67
# Optional API key used to read /api/logs when the g4f API is protected.
66
68
# Prefer G4F_API_KEY; keep G4F_PUBLIC_API_KEY as a backwards-compatible alias.
67
69
FEED_API_KEY = os.getenv("G4F_API_KEY") or os.getenv("G4F_PUBLIC_API_KEY", "")
68
MEMBERS_BASE = os.getenv("G4F_MEMBERS_BASE", "https://g4f.dev")
70
MEMBERS_BASE = os.getenv("G4F_MEMBERS_BASE", "https://g4f.space")
71
ERRORS_URL = os.getenv("G4F_ERRORS_URL", "https://g4f.space/api/errors")
69
72
FEED_POLL_INTERVAL = int(os.getenv("G4F_FEED_POLL_INTERVAL", "15"))
70
73
HEAVY_TOKEN_THRESHOLD = int(os.getenv("G4F_HEAVY_TOKEN_THRESHOLD", "10000"))
71
74
FEED_SUMMARY_INTERVAL = int(os.getenv("G4F_FEED_SUMMARY_INTERVAL", "3600"))
@@ -90,7 +93,7 @@ log = logging.getLogger("g4f-discord")
90
93
# ---------------------------------------------------------------------------
91
94
# g4f async client + MCP tool manager (shared across requests)
92
95
# ---------------------------------------------------------------------------
93
client = ClientFactory.create_async_client(provider="default",
96
client = ClientFactory.create_async_client(provider=G4FSpace,
94
97
api_key=os.getenv("G4F_API_KEY"),
95
98
media_provider=os.getenv("G4F_MEDIA_PROVIDER", AnyProvider))
96
99
mcp = MCPToolManager(enabled_tools=ENABLED_TOOLS)
@@ -624,6 +627,113 @@ async def tools(
624
627
)
625
628
626
629
630
# ---------------------------------------------------------------------------
631
# API errors command & helper
632
# ---------------------------------------------------------------------------
633
async def _fetch_api_errors(
634
url: str = ERRORS_URL,
635
limit: int = 5,
636
status_filter: Optional[int] = None,
637
) -> List[dict]:
638
"""Fetch recent API errors from the given errors endpoint."""
639
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
640
async with session.get(url) as resp:
641
if resp.status != 200:
642
raise RuntimeError(f"HTTP {resp.status} from {url}")
643
data = await resp.json()
644
645
entries = (
646
data.get("data", [])
647
if isinstance(data, dict)
648
else (data if isinstance(data, list) else [])
649
)
650
if status_filter is not None:
651
entries = [
652
e
653
for e in entries
654
if isinstance(e, dict) and e.get("status") == status_filter
655
]
656
return entries[:limit]
657
658
659
@bot.tree.command(
660
name="errors",
661
description="Fetch recent API errors from https://g4f.space/api/errors.",
662
)
663
@app_commands.describe(
664
limit="Number of recent errors to display (1-10, default: 5)",
665
status="Optional HTTP status code filter (e.g. 404, 403, 500)",
666
)
667
async def errors(
668
interaction: discord.Interaction,
669
limit: Optional[int] = 5,
670
status: Optional[int] = None,
671
):
672
await interaction.response.defer(thinking=True)
673
limit_val = max(1, min(int(limit or 5), 10))
674
675
try:
676
err_entries = await _fetch_api_errors(
677
url=ERRORS_URL, limit=limit_val, status_filter=status
678
)
679
except Exception as e:
680
log.exception("Failed to fetch API errors")
681
await interaction.followup.send(f"⚠️ Failed to fetch API errors: {e}")
682
return
683
684
if not err_entries:
685
msg = "No recent API errors found"
686
if status is not None:
687
msg += f" with status `{status}`"
688
msg += f" from `{ERRORS_URL}`."
689
await interaction.followup.send(msg)
690
return
691
692
embeds: List[discord.Embed] = []
693
for entry in err_entries:
694
embed = discord.Embed(
695
title=f"🚨 API Error (#{entry.get('id', '?')})",
696
color=0xDC3545,
697
)
698
method = entry.get("method", "POST")
699
pathname = entry.get("pathname", entry.get("path", "?"))
700
embed.add_field(name="Path", value=f"`{method} {pathname}`", inline=False)
701
embed.add_field(name="Status", value=str(entry.get("status", "?")), inline=True)
702
if entry.get("source"):
703
embed.add_field(name="Source", value=str(entry.get("source")), inline=True)
704
705
msg_str = entry.get("message")
706
if msg_str:
707
embed.add_field(
708
name="Message", value=_truncate(str(msg_str), 512), inline=False
709
)
710
711
if entry.get("request_id"):
712
embed.add_field(
713
name="Request ID", value=f"`{entry.get('request_id')}`", inline=True
714
)
715
if entry.get("timestamp"):
716
embed.add_field(
717
name="Timestamp", value=str(entry.get("timestamp")), inline=True
718
)
719
720
user_id = entry.get("user_id")
721
user_tier = entry.get("user_tier")
722
if user_id or user_tier:
723
embed.add_field(
724
name="User",
725
value=f"ID: `{user_id}` | Tier: `{user_tier}`",
726
inline=False,
727
)
728
729
embeds.append(embed)
730
731
await interaction.followup.send(
732
content=f"**Recent API Errors from `{ERRORS_URL}`** (showing {len(embeds)}):",
733
embeds=embeds,
734
)
735
736
627
737
@bot.event
628
738
async def on_message(message: discord.Message):
629
739
# Let slash commands etc. work as usual.
@@ -772,6 +882,7 @@ async def on_ready():
772
882
public_base=PUBLIC_BASE,
773
883
api_key=FEED_API_KEY,
774
884
members_base=MEMBERS_BASE or None,
885
errors_url=ERRORS_URL or None,
775
886
poll_interval=FEED_POLL_INTERVAL,
776
887
heavy_token_threshold=HEAVY_TOKEN_THRESHOLD,
777
888
summary_interval=FEED_SUMMARY_INTERVAL,
@@ -318,6 +318,7 @@ class LiveFeed(commands.Cog):
318
318
api_key: Optional[str],
319
319
public_base: str,
320
320
members_base: Optional[str],
321
errors_url: Optional[str] = "https://g4f.space/api/errors",
321
322
poll_interval: int = 15,
322
323
heavy_token_threshold: int = 10_000,
323
324
summary_interval: int = 3600,
@@ -329,6 +330,7 @@ class LiveFeed(commands.Cog):
329
330
self.public_base = public_base.rstrip("/")
330
331
self.api_key = api_key
331
332
self.members_base = members_base.rstrip("/") if members_base else None
333
self.errors_url = errors_url.rstrip("/") if errors_url else None
332
334
self.heavy_token_threshold = heavy_token_threshold
333
335
self.max_posts_per_cycle = max_posts_per_cycle
334
336
self._summary_interval = summary_interval
@@ -336,6 +338,8 @@ class LiveFeed(commands.Cog):
336
338
self._last_log_id: int = 0
337
339
self._initialized: bool = False
338
340
self._seen_user_keys: Set[str] = set()
341
self._seen_api_error_ids: Set[int] = set()
342
self._initialized_api_errors: bool = False
339
343
self._session: Optional[aiohttp.ClientSession] = None
340
344
341
345
# Rolling stats for periodic summary
@@ -397,6 +401,12 @@ class LiveFeed(commands.Cog):
397
401
except Exception:
398
402
log.exception("New users poll failed")
399
403
404
if self.errors_url:
405
try:
406
await self._poll_api_errors()
407
except Exception:
408
log.exception("API errors poll failed")
409
400
410
if time.time() - self._last_summary >= self._summary_interval:
401
411
await self._post_summary()
402
412
self._last_summary = time.time()
@@ -480,6 +490,65 @@ class LiveFeed(commands.Cog):
480
490
self._stats["new_users"] += 1
481
491
await self._post_new_user(user)
482
492
493
# ------------------------------------------------------------------
494
# API errors polling
495
# ------------------------------------------------------------------
496
497
async def _poll_api_errors(self) -> None:
498
"""Poll external API errors endpoint (e.g. https://g4f.space/api/errors)."""
499
if not self.errors_url:
500
return
501
session = self._get_session()
502
try:
503
async with session.get(self.errors_url) as resp:
504
if resp.status != 200:
505
return
506
data = await resp.json()
507
except (aiohttp.ClientError, asyncio.TimeoutError):
508
return
509
510
entries = (
511
data.get("data", [])
512
if isinstance(data, dict)
513
else (data if isinstance(data, list) else [])
514
)
515
if not entries:
516
return
517
518
if not self._initialized_api_errors:
519
for e in entries:
520
if isinstance(e, dict) and e.get("id") is not None:
521
self._seen_api_error_ids.add(e["id"])
522
self._initialized_api_errors = True
523
log.info("LiveFeed initialized API errors with %d entries", len(self._seen_api_error_ids))
524
return
525
526
new_entries = [
527
e
528
for e in entries
529
if isinstance(e, dict)
530
and e.get("id") is not None
531
and e.get("id") not in self._seen_api_error_ids
532
]
533
if not new_entries:
534
return
535
536
new_entries.sort(key=lambda e: e.get("id", 0))
537
538
posted = 0
539
for entry in new_entries:
540
eid = entry.get("id")
541
if eid is not None:
542
self._seen_api_error_ids.add(eid)
543
self._stats["errors"] += 1
544
if posted >= self.max_posts_per_cycle:
545
continue
546
if await self._post_api_error(entry):
547
posted += 1
548
549
if len(self._seen_api_error_ids) > 2000:
550
self._seen_api_error_ids = set(sorted(self._seen_api_error_ids)[-1000:])
551
483
552
# ------------------------------------------------------------------
484
553
# Entry dispatch
485
554
# ------------------------------------------------------------------
@@ -683,6 +752,50 @@ class LiveFeed(commands.Cog):
683
752
await self._send(embed)
684
753
return True
685
754
755
async def _post_api_error(self, entry: dict) -> bool:
756
"""Post an API error alert from external errors endpoint."""
757
eid = entry.get("id", "?")
758
embed = discord.Embed(
759
title=f"🚨 API Error (#{eid})",
760
color=COLORS["error"],
761
)
762
method = entry.get("method", "POST")
763
pathname = entry.get("pathname", entry.get("path", "?"))
764
embed.add_field(
765
name="Path",
766
value=f"`{method} {pathname}`",
767
inline=False,
768
)
769
embed.add_field(name="Status", value=str(entry.get("status", "?")), inline=True)
770
771
msg = entry.get("message")
772
if msg:
773
embed.add_field(name="Message", value=_truncate(str(msg), 512), inline=False)
774
775
req_id = entry.get("request_id")
776
if req_id:
777
embed.add_field(name="Request ID", value=f"`{req_id}`", inline=True)
778
779
source = entry.get("source")
780
if source:
781
embed.add_field(name="Source", value=str(source), inline=True)
782
783
ts = entry.get("timestamp")
784
if ts:
785
embed.add_field(name="Timestamp", value=str(ts), inline=True)
786
787
user_id = entry.get("user_id")
788
user_tier = entry.get("user_tier")
789
if user_id or user_tier:
790
embed.add_field(
791
name="User",
792
value=f"ID: `{user_id}` | Tier: `{user_tier}`",
793
inline=False,
794
)
795
796
await self._send(embed)
797
return True
798
686
799
async def _post_new_user(self, user: dict) -> None:
687
800
"""Post a new g4f.dev user announcement."""
688
801
username = user.get("username", "unknown")