diff --git a/bot/commands.py b/bot/commands.py index d05d739..eaf8e50 100644 --- a/bot/commands.py +++ b/bot/commands.py @@ -13,7 +13,7 @@ from agent.manager import manager from agent.scheduler import scheduler from agent.task_runner import task_runner from orchestrator.agent import agent -from orchestrator.tools import set_current_user, get_current_user, get_current_chat +from orchestrator.tools import set_current_user, get_current_chat logger = logging.getLogger(__name__) @@ -111,6 +111,18 @@ async def _cmd_new(user_id: str, args: str) -> str: conv_id = data.get("conv_id", "") agent._active_conv[user_id] = conv_id cwd = data.get("working_dir", working_dir) + + chat_id = get_current_chat() + if chat_id: + from bot.feishu import send_card, send_text, build_sessions_card + sessions = manager.list_sessions(user_id=user_id) + mode = "Direct 🟢" if agent.get_passthrough(user_id) else "Smart ⚪" + card = build_sessions_card(sessions, conv_id, mode) + await send_card(chat_id, "chat_id", card) + if initial_msg and data.get("response"): + await send_text(chat_id, "chat_id", data["response"]) + return "" + reply = f"✓ Created session `{conv_id}` in `{cwd}`" if parsed.timeout: reply += f" (timeout: {parsed.timeout}s)" @@ -124,17 +136,24 @@ async def _cmd_new(user_id: str, args: str) -> str: async def _cmd_status(user_id: str) -> str: """Show status: sessions and current mode.""" sessions = manager.list_sessions(user_id=user_id) - if not sessions: - return "No active sessions." - active = agent.get_active_conv(user_id) passthrough = agent.get_passthrough(user_id) + mode = "Direct 🟢" if passthrough else "Smart ⚪" + + chat_id = get_current_chat() + if chat_id: + from bot.feishu import send_card, build_sessions_card + card = build_sessions_card(sessions, active, mode) + await send_card(chat_id, "chat_id", card) + return "" + + if not sessions: + return "No active sessions." lines = ["**Your Sessions:**\n"] for i, s in enumerate(sessions, 1): marker = "→ " if s["conv_id"] == active else " " lines.append(f"{marker}{i}. `{s['conv_id']}` - `{s['cwd']}`") - status = "Direct 🟢" if passthrough else "Smart ⚪" - lines.append(f"\n**Mode:** {status}") + lines.append(f"\n**Mode:** {mode}") lines.append("Use `/switch ` to activate a session.") lines.append("Use `/direct` or `/smart` to change mode.") return "\n".join(lines) diff --git a/bot/feishu.py b/bot/feishu.py index 21586c3..13dace0 100644 --- a/bot/feishu.py +++ b/bot/feishu.py @@ -67,7 +67,7 @@ async def send_text(receive_id: str, receive_id_type: str, text: str) -> None: text: message content. """ parts = _split_message(text) - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() for i, part in enumerate(parts): logger.debug( @@ -108,59 +108,16 @@ async def send_text(receive_id: str, receive_id_type: str, text: str) -> None: await asyncio.sleep(0.3) -async def send_card(receive_id: str, receive_id_type: str, title: str, content: str, buttons: list[dict] | None = None) -> None: +async def send_card(receive_id: str, receive_id_type: str, card: dict) -> None: """ Send an interactive card message. Args: - receive_id: chat_id or open_id - receive_id_type: "chat_id" | "open_id" | "user_id" | "union_id" - title: Card title - content: Card content (markdown supported) - buttons: List of button dicts with "text" and "value" keys + receive_id: chat_id or open_id depending on receive_id_type. + receive_id_type: "chat_id" | "open_id" | "user_id" | "union_id". + card: Card content dict (Feishu card JSON schema). """ - elements = [ - { - "tag": "div", - "text": { - "tag": "lark_md", - "content": content, - }, - }, - ] - - if buttons: - actions = [] - for btn in buttons: - actions.append({ - "tag": "button", - "text": {"tag": "plain_text", "content": btn.get("text", "Button")}, - "type": "primary", - "value": btn.get("value", {}), - }) - elements.append({"tag": "action", "actions": actions}) - - card = { - "type": "template", - "data": { - "template_id": "AAqkz9****", - "template_variable": { - "title": title, - "elements": elements, - }, - }, - } - - card_content = { - "config": {"wide_screen_mode": True}, - "header": { - "title": {"tag": "plain_text", "content": title}, - "template": "blue", - }, - "elements": elements, - } - - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() request = ( CreateMessageRequest.builder() .receive_id_type(receive_id_type) @@ -168,7 +125,7 @@ async def send_card(receive_id: str, receive_id_type: str, title: str, content: CreateMessageRequestBody.builder() .receive_id(receive_id) .msg_type("interactive") - .content(json.dumps(card_content, ensure_ascii=False)) + .content(json.dumps(card, ensure_ascii=False)) .build() ) .build() @@ -185,6 +142,32 @@ async def send_card(receive_id: str, receive_id_type: str, title: str, content: logger.debug("Sent card to %s (%s)", receive_id, receive_id_type) +def build_sessions_card(sessions: list[dict], active_conv_id: str | None, mode: str) -> dict: + """Build a card showing all sessions with active marker and mode info.""" + if sessions: + lines = [] + for i, s in enumerate(sessions, 1): + marker = "→" if s["conv_id"] == active_conv_id else " " + started = "🟢" if s["started"] else "🟡" + lines.append(f"{marker} {i}. {started} `{s['conv_id']}` — `{s['cwd']}`") + sessions_md = "\n".join(lines) + else: + sessions_md = "_No active sessions_" + + content = f"{sessions_md}\n\n**Mode:** {mode}" + + return { + "config": {"wide_screen_mode": True}, + "header": { + "title": {"tag": "plain_text", "content": "Claude Code Sessions"}, + "template": "turquoise", + }, + "elements": [ + {"tag": "div", "text": {"tag": "lark_md", "content": content}}, + ], + } + + async def send_file(receive_id: str, receive_id_type: str, file_path: str, file_type: str = "stream") -> None: """ Upload a local file to Feishu and send it as a file message. @@ -198,7 +181,7 @@ async def send_file(receive_id: str, receive_id_type: str, file_path: str, file_ import os path = os.path.abspath(file_path) file_name = os.path.basename(path) - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() # Step 1: Upload file → get file_key with open(path, "rb") as f: @@ -259,27 +242,3 @@ async def send_file(receive_id: str, receive_id_type: str, file_path: str, file_ ) else: logger.debug("Sent file %r to %s (%s)", file_name, receive_id, receive_id_type) - - -def build_session_card(conv_id: str, cwd: str, started: bool) -> dict: - """Build a session status card.""" - status = "🟢 Active" if started else "🟡 Ready" - content = f"**Session ID:** `{conv_id}`\n**Directory:** `{cwd}`\n**Status:** {status}" - return { - "config": {"wide_screen_mode": True}, - "header": { - "title": {"tag": "plain_text", "content": "Claude Code Session"}, - "template": "turquoise", - }, - "elements": [ - {"tag": "div", "text": {"tag": "lark_md", "content": content}}, - {"tag": "hr"}, - { - "tag": "action", - "actions": [ - {"tag": "button", "text": {"tag": "plain_text", "content": "Continue"}, "type": "primary", "value": {"action": "continue", "conv_id": conv_id}}, - {"tag": "button", "text": {"tag": "plain_text", "content": "Close"}, "type": "default", "value": {"action": "close", "conv_id": conv_id}}, - ], - }, - ], - } diff --git a/host_client/main.py b/host_client/main.py index 94e7075..65bc991 100644 --- a/host_client/main.py +++ b/host_client/main.py @@ -130,7 +130,7 @@ class NodeClient: sessions = manager.list_sessions() active_sessions = [ - {"conv_id": s["conv_id"], "working_dir": s["working_dir"]} + {"conv_id": s["conv_id"], "working_dir": s["cwd"]} for s in sessions ] diff --git a/main.py b/main.py index 1a16ffc..482ee4b 100644 --- a/main.py +++ b/main.py @@ -91,7 +91,7 @@ async def startup_event() -> None: await manager.start() from agent.scheduler import scheduler await scheduler.start() - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() start_websocket_client(loop) logger.info("PhoneWork started") diff --git a/orchestrator/tools.py b/orchestrator/tools.py index 99ec6f2..fd948ba 100644 --- a/orchestrator/tools.py +++ b/orchestrator/tools.py @@ -301,7 +301,8 @@ class FileReadTool(BaseTool): async def _arun(self, path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> str: try: - file_path = _resolve_dir(path) + p = Path(path.strip()) + file_path = _resolve_dir(str(p.parent)) / p.name if not p.is_absolute() else p.resolve() if not file_path.is_file(): return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False) @@ -342,7 +343,8 @@ class FileWriteTool(BaseTool): async def _arun(self, path: str, content: str, mode: Optional[str] = "overwrite") -> str: try: - file_path = _resolve_dir(path) + p = Path(path.strip()) + file_path = (_resolve_dir(str(p.parent)) / p.name) if not p.is_absolute() else p.resolve() file_path.parent.mkdir(parents=True, exist_ok=True) write_mode = "a" if mode == "append" else "w" @@ -484,7 +486,8 @@ class FileSendTool(BaseTool): async def _arun(self, path: str) -> str: try: - file_path = _resolve_dir(path) + p = Path(path.strip()) + file_path = _resolve_dir(str(p.parent)) / p.name if not p.is_absolute() else p.resolve() if not file_path.is_file(): return json.dumps({"error": f"Not a file: {path}"}, ensure_ascii=False) diff --git a/router/main.py b/router/main.py index 30924cf..57527a9 100644 --- a/router/main.py +++ b/router/main.py @@ -43,6 +43,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI: @app.get("/health") async def health(): + from router.rpc import get_pending_count nodes = registry.list_nodes() online_nodes = [n for n in nodes if n["status"] == "online"] return { @@ -50,7 +51,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI: "nodes": nodes, "online_nodes": len(online_nodes), "total_nodes": len(nodes), - "pending_requests": 0, + "pending_requests": get_pending_count(), } @app.get("/nodes") @@ -64,7 +65,7 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI: @app.on_event("startup") async def startup(): import asyncio - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() start_websocket_client(loop) logger.info("Router started") diff --git a/router/rpc.py b/router/rpc.py index 9dc793b..d319f95 100644 --- a/router/rpc.py +++ b/router/rpc.py @@ -52,7 +52,7 @@ async def forward( raise RuntimeError(f"Node not connected: {node_id}") request_id = str(uuid.uuid4()) - future: asyncio.Future[str] = asyncio.get_event_loop().create_future() + future: asyncio.Future[str] = asyncio.get_running_loop().create_future() _pending_requests[request_id] = future request = ForwardRequest(