211 lines
5.8 KiB
Python
211 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Team Board Agent — Coder.
|
|
|
|
Connects to Tracker via WebSocket, listens for tasks, executes them via OpenClaw.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import signal
|
|
import sys
|
|
|
|
import websockets
|
|
|
|
from config import (
|
|
TRACKER_WS_URL,
|
|
AGENT_TOKEN,
|
|
AGENT_NAME,
|
|
AGENT_SLUG,
|
|
HEARTBEAT_INTERVAL,
|
|
)
|
|
from brain import think
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger("agent")
|
|
|
|
# State
|
|
current_tasks: list[str] = []
|
|
status = "idle"
|
|
running = True
|
|
|
|
|
|
async def send_json(ws, data: dict):
|
|
await ws.send(json.dumps(data))
|
|
|
|
|
|
async def heartbeat(ws):
|
|
"""Send periodic heartbeat to Tracker."""
|
|
while running:
|
|
try:
|
|
await send_json(ws, {
|
|
"event": "agent.heartbeat",
|
|
"status": status,
|
|
"current_tasks": current_tasks,
|
|
})
|
|
except Exception:
|
|
break
|
|
await asyncio.sleep(HEARTBEAT_INTERVAL)
|
|
|
|
|
|
async def handle_task_assigned(ws, data: dict):
|
|
"""Handle an assigned task — execute it."""
|
|
global status
|
|
task_id = data.get("task_id")
|
|
task_title = data.get("title", "Без названия")
|
|
task_description = data.get("description")
|
|
project_name = data.get("project_name", "")
|
|
|
|
logger.info("Task assigned: %s — %s", task_id, task_title)
|
|
|
|
status = "busy"
|
|
current_tasks.append(task_id)
|
|
|
|
try:
|
|
# Take the task
|
|
await send_json(ws, {"event": "task.take", "task_id": task_id})
|
|
|
|
# Think (send to OpenClaw)
|
|
result = await think(task_title, task_description, project_name)
|
|
|
|
# Post result as comment
|
|
await send_json(ws, {
|
|
"event": "task.comment",
|
|
"task_id": task_id,
|
|
"content": result,
|
|
"sender_type": "agent",
|
|
"sender_name": AGENT_NAME,
|
|
})
|
|
|
|
# Complete the task
|
|
await send_json(ws, {"event": "task.complete", "task_id": task_id})
|
|
logger.info("Task completed: %s", task_id)
|
|
|
|
except Exception as e:
|
|
logger.error("Error processing task %s: %s", task_id, e)
|
|
# Comment the error
|
|
await send_json(ws, {
|
|
"event": "task.comment",
|
|
"task_id": task_id,
|
|
"content": f"❌ Ошибка: {e}",
|
|
"sender_type": "agent",
|
|
"sender_name": AGENT_NAME,
|
|
})
|
|
finally:
|
|
if task_id in current_tasks:
|
|
current_tasks.remove(task_id)
|
|
status = "idle" if not current_tasks else "busy"
|
|
|
|
|
|
async def handle_chat_message(ws, data: dict):
|
|
"""Handle incoming chat message — respond if mentioned."""
|
|
content = data.get("content", "")
|
|
sender_name = data.get("sender_name", "")
|
|
chat_id = data.get("chat_id")
|
|
|
|
# Only respond if mentioned
|
|
if f"@{AGENT_SLUG}" not in content and f"@{AGENT_NAME}" not in content.lower():
|
|
return
|
|
|
|
logger.info("Mentioned in chat by %s: %s", sender_name, content[:100])
|
|
|
|
# Simple response via brain
|
|
result = await think(
|
|
f"Сообщение от {sender_name}: {content}",
|
|
None,
|
|
"чат",
|
|
)
|
|
|
|
await send_json(ws, {
|
|
"event": "chat.send",
|
|
"chat_id": chat_id,
|
|
"content": result,
|
|
"sender_type": "agent",
|
|
"sender_name": AGENT_NAME,
|
|
})
|
|
|
|
|
|
async def connect():
|
|
"""Main connection loop with auto-reconnect."""
|
|
while running:
|
|
try:
|
|
url = f"{TRACKER_WS_URL}?client_type=agent&client_id={AGENT_SLUG}"
|
|
logger.info("Connecting to Tracker: %s", url)
|
|
|
|
async with websockets.connect(url) as ws:
|
|
logger.info("Connected to Tracker ✅")
|
|
|
|
# Auth
|
|
await send_json(ws, {
|
|
"event": "auth",
|
|
"token": AGENT_TOKEN,
|
|
})
|
|
|
|
# Start heartbeat
|
|
hb_task = asyncio.create_task(heartbeat(ws))
|
|
|
|
try:
|
|
async for raw in ws:
|
|
try:
|
|
msg = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
event = msg.get("event")
|
|
logger.debug("Event: %s", event)
|
|
|
|
if event == "auth.ok":
|
|
logger.info("Authenticated. Init: %s", json.dumps(msg.get("init", {}), ensure_ascii=False)[:200])
|
|
|
|
elif event == "task.assigned":
|
|
asyncio.create_task(handle_task_assigned(ws, msg))
|
|
|
|
elif event == "chat.message":
|
|
asyncio.create_task(handle_chat_message(ws, msg))
|
|
|
|
elif event == "agent.heartbeat.ack":
|
|
pass # OK
|
|
|
|
elif event == "error":
|
|
logger.warning("Error from Tracker: %s", msg.get("message"))
|
|
|
|
else:
|
|
logger.debug("Unhandled event: %s", event)
|
|
|
|
finally:
|
|
hb_task.cancel()
|
|
|
|
except (websockets.ConnectionClosed, ConnectionRefusedError, OSError) as e:
|
|
logger.warning("Connection lost: %s. Reconnecting in 5s...", e)
|
|
await asyncio.sleep(5)
|
|
except Exception as e:
|
|
logger.error("Unexpected error: %s. Reconnecting in 10s...", e)
|
|
await asyncio.sleep(10)
|
|
|
|
|
|
def main():
|
|
global running
|
|
|
|
def stop(*_):
|
|
global running
|
|
running = False
|
|
logger.info("Shutting down...")
|
|
|
|
signal.signal(signal.SIGINT, stop)
|
|
signal.signal(signal.SIGTERM, stop)
|
|
|
|
logger.info("🤖 Agent '%s' (%s) starting...", AGENT_NAME, AGENT_SLUG)
|
|
|
|
if not AGENT_TOKEN:
|
|
logger.error("AGENT_TOKEN is required!")
|
|
sys.exit(1)
|
|
|
|
asyncio.run(connect())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|