refactor(cleanup): v0.5.0 Collector 제거 + dead code 정리 + HttpBridgeContext 버그 수정

- DELETE collector.py (523줄)
- main.py: BOT_MODE=remote 분기 제거
- gateway.py: Collector REST 6개 endpoint 제거 (311→168줄)
- bridge.py: RemoteTransport 제거 (480→270줄)
- config.py: REMOTE_BRIDGE_URL 제거
- extension.ts: dead code 4개 + stale module vars 제거
- step-probe.ts: getStepProbeContext() 추가, autoApproveEnabled 제거
- FIX: HttpBridgeContext stale primitive (getter 패턴으로 수정)
- ADD: extension.log rotation (10MB→2MB tail)
- docs: architecture.md, tech-stack.md, known-issues.md 업데이트
This commit is contained in:
Variet Worker
2026-03-18 11:08:59 +09:00
parent 4a5521dcc3
commit e7631177f8
15 changed files with 65 additions and 989 deletions

View File

@@ -1,20 +1,10 @@
"""Gateway HTTP API + WebSocket Hub — receives data from Collectors and Extensions.
"""Gateway HTTP API + WebSocket Hub — serves WebSocket Hub and diagnostics.
Runs alongside the Discord bot in the server Docker container.
Supports both:
- REST API: for legacy Collectors (HTTP polling)
- WebSocket: for direct Extension connections (real-time)
Endpoints:
GET /ws — WebSocket endpoint (Extension direct connection)
POST /api/pending — Collector pushes a new approval request
GET /api/pending — List all pending requests (for diagnostics)
POST /api/response/{rid} — Collector polls for response (or Gateway pushes)
GET /api/response/{rid} — Get response for a specific request
POST /api/chat — Collector pushes a chat snapshot
POST /api/register — Collector registers session → project mapping
POST /api/command — Gateway pushes command to specific collector
GET /api/commands/{project} — Collector polls for commands
GET /health — Health check
GET /hub/status — WebSocket Hub diagnostics
"""
@@ -60,15 +50,9 @@ class GatewayAPI:
# WebSocket endpoint (no auth middleware — Hub handles its own auth)
self.app.router.add_get("/ws", self._ws_handler)
self.app.router.add_get("/hub/status", self._hub_status)
# Legacy REST endpoints (Collector compatibility)
# REST endpoints
self.app.router.add_get("/health", self._health)
self.app.router.add_post("/api/pending", self._post_pending)
self.app.router.add_get("/api/pending", self._list_pending)
self.app.router.add_get("/api/response/{rid}", self._get_response)
self.app.router.add_post("/api/chat", self._post_chat)
self.app.router.add_post("/api/register", self._post_register)
self.app.router.add_get("/api/commands/{project}", self._get_commands)
self.app.router.add_post("/api/event", self._post_event)
# ─── WebSocket Handler ───
@@ -144,25 +128,7 @@ class GatewayAPI:
status["hub_connections"] = len(self.hub.connections)
return web.json_response(status)
# ─── Pending Approvals (Collector → Gateway → Discord) ───
async def _post_pending(self, request: web.Request) -> web.Response:
"""Collector pushes a pending approval request."""
try:
data = await request.json()
rid = data.get("request_id", f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}")
data["request_id"] = rid
data.setdefault("timestamp", time.time())
data.setdefault("status", "pending")
# Write to bridge pending dir (bot's scanner will pick it up)
self.bot.bridge.transport.write_json("pending", f"{rid}.json", data)
logger.info(f"[GATEWAY] pending received: {rid[:12]} project={data.get('project_name', '?')}")
return web.json_response({"ok": True, "request_id": rid})
except Exception as e:
logger.error(f"[GATEWAY] pending error: {e}")
return web.json_response({"ok": False, "error": str(e)}, status=400)
# ─── Pending List (Diagnostics) ───
async def _list_pending(self, request: web.Request) -> web.Response:
"""List all pending requests (diagnostics)."""
@@ -174,88 +140,14 @@ class GatewayAPI:
"status": r.status,
} for r in requests])
# ─── Responses (Discord → Gateway → Collector) ───
async def _get_response(self, request: web.Request) -> web.Response:
"""Collector polls for a response to a specific pending request."""
rid = request.match_info["rid"]
data = self.bot.bridge.transport.read_json("response", f"{rid}.json")
if data is None:
return web.json_response({"waiting": True, "request_id": rid})
# Serve response and delete both response + pending files (one-time consumption)
self.bot.bridge.transport.delete_file("response", f"{rid}.json")
self.bot.bridge.transport.delete_file("pending", f"{rid}.json") # Bug 2 fix
return web.json_response(data)
# ─── Chat Snapshots (Collector → Gateway → Discord) ───
async def _post_chat(self, request: web.Request) -> web.Response:
"""Collector pushes a chat snapshot for relay to Discord."""
try:
data = await request.json()
project = data.get("project_name", "")
content = data.get("content", "")
attached_files = data.get("attached_files", [])
if not project or (not content and not attached_files):
return web.json_response({"ok": False, "error": "project_name and content/attached_files required"}, status=400)
# Write to chat_snapshots dir for bot's scanner
snap_dir = self.bot.bridge.transport.bridge_dir / "chat_snapshots" if hasattr(self.bot.bridge.transport, 'bridge_dir') else None
if snap_dir:
snap_dir.mkdir(parents=True, exist_ok=True)
snap_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
snap_data = {
"id": snap_id,
"project_name": project,
"content": content,
"timestamp": time.time(),
}
if attached_files:
snap_data["attached_files"] = attached_files
(snap_dir / f"{snap_id}.json").write_text(
json.dumps(snap_data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
af_info = f" +{len(attached_files)} files" if attached_files else ""
logger.info(f"[GATEWAY] chat received: project={project} len={len(content)}{af_info}")
return web.json_response({"ok": True})
except Exception as e:
logger.error(f"[GATEWAY] chat error: {e}")
return web.json_response({"ok": False, "error": str(e)}, status=400)
# ─── Registration (Collector → Gateway) ───
async def _post_register(self, request: web.Request) -> web.Response:
"""Collector registers session → project mapping."""
try:
data = await request.json()
session_id = data.get("conversation_id", "")
project = data.get("project_name", "")
if session_id and project:
self.bot.conv_to_project[session_id] = project
logger.info(f"[GATEWAY] registered: {session_id[:8]}{project}")
return web.json_response({"ok": True})
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=400)
# ─── Commands (Gateway → Collector) ───
async def _get_commands(self, request: web.Request) -> web.Response:
"""Collector polls for commands (e.g. !auto, !stop, text messages)."""
project = request.match_info["project"]
commands = self._commands.pop(project, [])
return web.json_response({"commands": commands})
# ─── Commands (Legacy fallback: Bot → Extension via HTTP when Hub unavailable) ───
def push_command(self, project: str, command: dict):
"""Bot pushes a command for a Collector to pick up."""
"""Bot pushes a command for Extension to pick up (Hub fallback)."""
if project not in self._commands:
self._commands[project] = []
command.setdefault("_ts", time.time()) # TTL tracking
self._commands[project].append(command)
# Auto-cleanup stale commands (Security 3: memory leak prevention)
self._cleanup_stale_commands()
def _cleanup_stale_commands(self):
@@ -269,35 +161,6 @@ class GatewayAPI:
if not self._commands[project]:
del self._commands[project]
# ─── Brain Events (Collector → Gateway → Discord) ───
async def _post_event(self, request: web.Request) -> web.Response:
"""Collector pushes a brain event (file change) for relay to Discord."""
try:
data = await request.json()
from watcher import BrainEvent, EventType
event_type_str = data.get("event_type", "file_changed")
event_type = EventType(event_type_str)
event = BrainEvent(
event_type=event_type,
conversation_id=data.get("conversation_id", ""),
file_name=data.get("file_name", ""),
file_path=Path(data["file_path"]) if data.get("file_path") else None,
content=data.get("content", ""),
timestamp=data.get("timestamp", time.time()),
)
# Inject into bot's event queue (same path as local mode)
await self.bot.event_queue.put(event)
logger.info(f"[GATEWAY] event received: {event_type_str} {event.file_name} conv={event.conversation_id[:8]}")
return web.json_response({"ok": True})
except Exception as e:
logger.error(f"[GATEWAY] event error: {e}")
return web.json_response({"ok": False, "error": str(e)}, status=400)
# ─── Run ───
async def start(self):