Gap 1: Discord→Extension 명령어 깨짐 - bot.py: _write_command() 래퍼 — gateway.push_command()도 호출 - main.py: bot.gateway 연결 - 슬래시 명령어 + on_message 모두 _write_command 사용 Gap 2: Chat snapshot 미전달 - collector.py: _forward_chat_snapshots_loop 추가 Gap 3: Session registration 미전달 - collector.py: _forward_registrations_loop 추가
135 lines
4.4 KiB
Python
135 lines
4.4 KiB
Python
"""Gravity Control — Antigravity Discord Bridge.
|
|
|
|
Entry point that runs the brain watcher and Discord bot together.
|
|
"""
|
|
|
|
import asyncio
|
|
import io
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from config import Config
|
|
from watcher import BrainWatcher
|
|
from bot import GravityBot
|
|
|
|
# Logging setup (UTF-8 forced for Windows cp949 compatibility)
|
|
_utf8_stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
handlers=[
|
|
logging.StreamHandler(_utf8_stdout),
|
|
logging.FileHandler("gravity_control.log", encoding="utf-8"),
|
|
],
|
|
)
|
|
logger = logging.getLogger("gravity_control")
|
|
|
|
|
|
async def main():
|
|
"""Run the bridge: watcher + Discord bot."""
|
|
|
|
# Validate config
|
|
errors = Config.validate()
|
|
if errors:
|
|
for e in errors:
|
|
logger.error(f"Config error: {e}")
|
|
logger.error("Fix configuration issues and restart.")
|
|
sys.exit(1)
|
|
|
|
logger.info("=" * 50)
|
|
logger.info("Gravity Control — Antigravity Discord Bridge")
|
|
logger.info("=" * 50)
|
|
logger.info(f"Brain path: {Config.BRAIN_PATH}")
|
|
logger.info(f"Debounce: {Config.DEBOUNCE_SECONDS}s")
|
|
logger.info(f"Bot mode: {Config.BOT_MODE}")
|
|
|
|
# Shared event queue
|
|
event_queue = asyncio.Queue()
|
|
|
|
# Get the running loop
|
|
loop = asyncio.get_running_loop()
|
|
|
|
# ── Collector mode: no Discord bot, just relay local ↔ Gateway ──
|
|
if Config.BOT_MODE == "remote":
|
|
from bridge import LocalTransport, RemoteTransport
|
|
from collector import CollectorBridge
|
|
|
|
if not Config.REMOTE_BRIDGE_URL:
|
|
logger.error("REMOTE_BRIDGE_URL is required for remote (Collector) mode")
|
|
sys.exit(1)
|
|
|
|
bridge_dir = Config.BRAIN_PATH.parent / "bridge"
|
|
local = LocalTransport(bridge_dir)
|
|
local.ensure_dirs()
|
|
remote = RemoteTransport(Config.REMOTE_BRIDGE_URL, api_key=Config.GATEWAY_API_KEY)
|
|
|
|
collector = CollectorBridge(local, remote, project_name=Config.PROJECT_NAME,
|
|
event_queue=event_queue)
|
|
logger.info(f"Collector mode: {Config.REMOTE_BRIDGE_URL}")
|
|
|
|
# Optionally start watcher for brain events (local display only)
|
|
watcher = BrainWatcher(event_queue, loop)
|
|
|
|
try:
|
|
watcher.start()
|
|
logger.info(f"Watcher started, {len(watcher.known_sessions)} existing sessions")
|
|
await collector.start()
|
|
except KeyboardInterrupt:
|
|
logger.info("Received keyboard interrupt")
|
|
except Exception as e:
|
|
logger.error(f"Fatal error: {e}", exc_info=True)
|
|
finally:
|
|
await collector.stop()
|
|
watcher.stop()
|
|
logger.info("Collector shutdown complete")
|
|
return
|
|
|
|
# ── Local / Gateway mode ──
|
|
|
|
# Create components
|
|
watcher = None
|
|
if Config.BOT_MODE != 'gateway':
|
|
watcher = BrainWatcher(event_queue, loop)
|
|
bot = GravityBot(event_queue)
|
|
|
|
try:
|
|
# Start watcher (local mode only — gateway receives data via HTTP)
|
|
if watcher:
|
|
watcher.start()
|
|
logger.info(f"Watcher started, {len(watcher.known_sessions)} existing sessions")
|
|
else:
|
|
logger.info("Gateway mode — watcher disabled (data via HTTP API)")
|
|
|
|
# Start Gateway HTTP API (gateway mode)
|
|
if Config.BOT_MODE == 'gateway':
|
|
from gateway import GatewayAPI
|
|
gateway_port = int(os.environ.get('GATEWAY_PORT', '8585'))
|
|
gateway = GatewayAPI(bot, port=gateway_port, api_key=Config.GATEWAY_API_KEY)
|
|
bot.gateway = gateway # Enable _write_command → gateway.push_command
|
|
await gateway.start()
|
|
logger.info(f"Gateway API running on port {gateway_port}")
|
|
|
|
# Run Discord bot (blocks until bot disconnects)
|
|
await bot.start(Config.DISCORD_TOKEN)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("Received keyboard interrupt")
|
|
except Exception as e:
|
|
logger.error(f"Fatal error: {e}", exc_info=True)
|
|
finally:
|
|
# Cleanup
|
|
if watcher:
|
|
watcher.stop()
|
|
if not bot.is_closed():
|
|
await bot.close()
|
|
logger.info("Gravity Control shutdown complete")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
asyncio.run(main())
|
|
except KeyboardInterrupt:
|
|
pass
|