XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

Update discord bot

46238695
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

2 个文件 +226 -2
Modified projects/discord-bot/bot.py +113 -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,
Modified projects/discord-bot/live_feed.py +113 -0
@@ -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")