#!/usr/bin/env python3 """BotOps — aibox 智能体的现代化 TUI 前端(Textual)。 复刻并增强 aibox/client.py: - AI 回复流式 Markdown 渲染(Markdown 部件增量追加,无闪烁) - 断线指数退避自动重连(/disconnect 抑制) - 任务事件独立侧栏(task_process / task_status / task_error) - Ctrl+C 中断生成、Ctrl+Q 退出、Ctrl+L 清屏 用法: python app.py [ws://host:port] # 默认 ws://localhost:8765 """ import sys from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Horizontal from textual.widgets import Footer, Header, Input, RichLog from commands import CommandRegistry from widgets.chat_view import ChatView from widgets.status_bar import StatusBar from ws_client import BotWSClient DEFAULT_URI = "ws://localhost:8765" class BotOpsApp(App): TITLE = "BotOps" SUB_TITLE = "aibox TUI" CSS = """ #chat { width: 3fr; border: solid $primary; } #tasks { width: 1fr; border: solid $secondary; } .msg-user { color: cyan; padding: 0 1; margin-top: 1; } .msg-ai { padding: 0 1; } .msg-system { color: $text-muted; padding: 0 1; } .msg-error { color: red; padding: 0 1; } .msg-task { color: yellow; padding: 0 1; } #status { height: 1; } """ BINDINGS = [ Binding("ctrl+c", "interrupt", "中断", show=True), Binding("ctrl+q", "quit", "退出", show=True), Binding("ctrl+l", "clear", "清屏", show=True), ] def __init__(self, uri: str = DEFAULT_URI): super().__init__() self.ws = BotWSClient(uri) self.ws.on_message = self._on_ws_message self.ws.on_state = self._on_ws_state self.registry = CommandRegistry(self) # ── 布局 ───────────────────────────────────────────────────────────── def compose(self) -> ComposeResult: yield Header() with Horizontal(): yield ChatView(id="chat") yield RichLog(id="tasks", max_lines=1000) yield Input(placeholder="输入消息,/help 查看命令,Enter 发送", id="input") yield StatusBar(id="status") yield Footer() async def on_mount(self): self.query_one("#input", Input).focus() self._refresh_status("") self.run_worker(self.connect_to(self.ws.uri)) @property def chat(self) -> ChatView: return self.query_one("#chat", ChatView) @property def tasks(self) -> RichLog: return self.query_one("#tasks", RichLog) # ── 连接管理 ────────────────────────────────────────────────────────── async def connect_to(self, uri: str): if self.ws.connected: await self.ws.disconnect() self.ws.uri = uri await self.notify_system(f"正在连接 {uri} …") await self.ws.connect() async def disconnect(self): await self.ws.disconnect() async def send_interrupt(self): if await self.ws.send_interrupt(): await self.notify_system("🛑 已发送中断信号") else: await self.notify_system("未连接到服务器", level="error") # ── UI 操作 ─────────────────────────────────────────────────────────── async def notify_system(self, text: str, level: str = "info"): css = {"info": "msg-system", "error": "msg-error", "task": "msg-task"}.get(level, "msg-system") await self.chat.add_system(text, css) async def clear_chat(self): await self.chat.reset() def _refresh_status(self, extra: str): self.query_one("#status", StatusBar).set_state( self.ws.connected, self.ws.uri if self.ws.connected else "", extra) # ── 输入处理 ────────────────────────────────────────────────────────── async def on_input_submitted(self, event: Input.Submitted): text = event.value.strip() event.input.value = "" if not text: return if text.startswith("/") and await self.registry.execute(text): return if not self.ws.connected: await self.notify_system("未连接到服务器,先 /connect", level="error") return await self.chat.add_user(text) await self.chat.start_ai() # 预建 AI 消息框,即时反馈 await self.ws.send_message(text) # ── 快捷键 ──────────────────────────────────────────────────────────── async def action_interrupt(self): await self.send_interrupt() async def action_clear(self): await self.clear_chat() def action_quit(self): self.exit() # ── aibox 协议消息路由(对应 client.py 的 _handle_message)───────────── async def _on_ws_message(self, data: dict): t = data.get("type", "") content = data.get("content", "") if t == "chunk": if content.startswith("🔧 调用工具"): await self.notify_system(content, level="task") elif content.startswith(("✅ 对话记忆已保存", "💾 保存记忆", "❌", "⚠️")): await self.notify_system(content, level="task") else: await self.chat.append_ai(content) elif t == "complete": await self.chat.finish_ai(content or self.chat.ai_text) elif t == "dialogue_ended": await self.chat.finish_ai(self.chat.ai_text) if content: await self.notify_system(f"✅ {content}") elif t == "task_process": sub = data.get("msg_type", "") title = data.get("task_title", "未知任务") if sub == "chunk": tail = str(data.get("content", ""))[-200:] if tail: self.tasks.write(f"⚡ {title}: {tail}") elif sub == "complete": self.tasks.write(f"✅ 任务完成: {title}") elif t == "task_status": icon = {"executing": "⚙️", "complete": "✅", "failed": "❌", "added": "➕", "batch_start": "🚀", "batch_complete": "🏁" }.get(data.get("status", ""), "📋") self.tasks.write( f"{icon} [{data.get('task_id', '?')}] {data.get('title', '')}: " f"{data.get('message', content)}") elif t == "task_error": self.tasks.write( f"❌ [{data.get('task_id', '?')}] {data.get('task_title', '?')}: " f"{data.get('error', '未知错误')}") elif t == "interrupt_ack": await self.notify_system(f"🛑 {content}") elif t == "error": await self.notify_system(f"❌ {content}", level="error") elif t == "info": await self.notify_system(f"ℹ️ {content}") elif t == "command_result": await self.notify_system(content) elif t == "connection_ack": await self.notify_system(f"✅ {content}") elif t == "notification": await self.notify_system(f"🔔 {content}") elif content: await self.chat.append_ai(content) # ── 连接状态回调 ────────────────────────────────────────────────────── async def _on_ws_state(self, state: str, detail: str): if state == "connected": self._refresh_status("") await self.notify_system(f"✅ 已连接到 {self.ws.uri}") elif state == "disconnected": self._refresh_status("") await self.notify_system(f"❌ {detail}") elif state == "reconnecting": self._refresh_status(detail) elif state == "error": self._refresh_status("") await self.notify_system(detail, level="error") def main(): uri = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URI BotOpsApp(uri).run() if __name__ == "__main__": main()