import os import threading import traceback from datetime import datetime from app.infrastructure.wechat_multi_chat_bot import WechatMultiChatBot class BotController: def __init__(self): self._lock = threading.RLock() self._thread = None self._bot = None self._status = "stopped" self._last_error = "" self._started_at = "" self._stopped_at = "" self._listeners = {} self._listener_seq = 0 def start(self, backend_url): with self._lock: if self._thread and self._thread.is_alive(): return self._status_payload_locked() self._status = "starting" self._last_error = "" self._stopped_at = "" os.environ["BACKEND_URL"] = backend_url self._thread = threading.Thread(target=self._run, daemon=True, name="WechatBotThread") self._thread.start() status = self._status_payload_locked() listeners = list(self._listeners.values()) self._notify_status_listeners(listeners, status) return status def stop(self): with self._lock: if not self._thread or not self._thread.is_alive(): self._status = "stopped" self._bot = None self._stopped_at = self._now() status = self._status_payload_locked() listeners = list(self._listeners.values()) self._notify_status_listeners(listeners, status) return status self._status = "stopping" bot = self._bot status = self._status_payload_locked() listeners = list(self._listeners.values()) self._notify_status_listeners(listeners, status) if bot: bot.stop() return status def status(self): listeners = None with self._lock: if self._status in {"running", "starting", "stopping"} and self._thread and not self._thread.is_alive(): if self._status != "error": self._status = "stopped" self._bot = None self._stopped_at = self._stopped_at or self._now() listeners = list(self._listeners.values()) status = self._status_payload_locked() if listeners is not None: self._notify_status_listeners(listeners, status) return status def add_status_listener(self, callback, emit_initial=False): with self._lock: self._listener_seq += 1 listener_id = self._listener_seq self._listeners[listener_id] = callback status = self._status_payload_locked() if emit_initial: try: callback(status) except Exception: pass return listener_id def remove_status_listener(self, listener_id): with self._lock: self._listeners.pop(listener_id, None) def _run(self): try: bot = WechatMultiChatBot() with self._lock: self._bot = bot self._status = "running" self._started_at = self._now() status = self._status_payload_locked() listeners = list(self._listeners.values()) self._notify_status_listeners(listeners, status) bot.run_forever() with self._lock: self._status = "stopped" self._bot = None self._stopped_at = self._now() status = self._status_payload_locked() listeners = list(self._listeners.values()) self._notify_status_listeners(listeners, status) except Exception: with self._lock: self._status = "error" self._bot = None self._last_error = traceback.format_exc() self._stopped_at = self._now() status = self._status_payload_locked() listeners = list(self._listeners.values()) self._notify_status_listeners(listeners, status) def _status_payload_locked(self): return { "status": self._status, "running": self._status == "running", "last_error": self._last_error, "started_at": self._started_at, "stopped_at": self._stopped_at, } def _notify_status_listeners(self, listeners, status): for callback in listeners: try: callback(status) except Exception: pass def _now(self): return datetime.now().strftime("%Y-%m-%d %H:%M:%S") bot_controller = BotController()