From 9173aa6094f91e9013a45a17ed090c376843079d Mon Sep 17 00:00:00 2001 From: "Yuyao Huang (Sam)" Date: Sun, 29 Mar 2026 16:46:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(router):=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84=EF=BC=8C=E5=B0=86=E4=B8=BB=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E7=A7=BB=E8=87=B3router/main.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除旧的main.py文件,将FastAPI应用创建逻辑集中在router/main.py中 添加直接运行router/main.py的支持 更新README.md以反映新的项目结构和使用方式 添加websocket连接测试脚本test_websocket.py --- README.md | 18 ++++--- main.py | 117 ---------------------------------------------- router/main.py | 15 ++++++ test_websocket.py | 38 +++++++++++++++ 4 files changed, 61 insertions(+), 127 deletions(-) delete mode 100644 main.py create mode 100644 test_websocket.py diff --git a/README.md b/README.md index 34ec76e..70a0790 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,8 @@ PhoneWork uses a **Router + Host Client** architecture that supports both single | Module | Purpose | |--------|---------| | `standalone.py` | Single-process entry point: runs router + host client together | -| `main.py` | FastAPI entry point for router-only mode | +| `router/main.py` | FastAPI app factory, mounts `/ws/node` endpoint, can be run directly | | `shared/protocol.py` | Wire protocol for router-host communication | -| `router/main.py` | FastAPI app factory, mounts `/ws/node` endpoint | | `router/nodes.py` | Node registry, connection management, user-to-node mapping | | `router/ws.py` | WebSocket endpoint for host clients, heartbeat, message routing | | `router/rpc.py` | Request correlation with asyncio.Future, timeout handling | @@ -140,13 +139,13 @@ OPENAI_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OPENAI_MODEL: glm-4.7 # Server configuration -# Only used in router mode (python main.py) or standalone mode (python standalone.py) +# Only used in router mode (python -m router.main) or standalone mode (python standalone.py) # Default: 8000 PORT: 8000 # Root directory for all project sessions (absolute path) # Only used in standalone mode (python standalone.py) -# In router mode (python main.py), this field is ignored +# In router mode (python -m router.main), this field is ignored WORKING_DIR: C:/Users/yourname/projects # Allowlist of Feishu open_ids that may use the bot. @@ -159,8 +158,7 @@ ALLOWED_OPEN_IDS: METASO_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Optional: Multi-host mode configuration -# Set ROUTER_MODE to true to enable router mode (deploy on public VPS) -ROUTER_MODE: false +# Set ROUTER_SECRET for authentication between router and host clients ROUTER_SECRET: your-shared-secret-for-router-host-auth ``` @@ -252,8 +250,8 @@ python standalone.py **Router** (public VPS or any reachable machine — runs the Feishu bot): ```bash -# keyring.yaml must have ROUTER_MODE: true and ROUTER_SECRET set -python main.py +# keyring.yaml must have ROUTER_SECRET set +python -m router.main ``` **Host client** (your dev machine behind NAT — runs Claude Code): @@ -389,8 +387,8 @@ Runs both router and host client in one process. Identical UX to pre-M3 setup. **Router Mode (Public VPS):** ```bash -# Set ROUTER_MODE: true in keyring.yaml -python main.py +# Set ROUTER_SECRET in keyring.yaml +python -m router.main ``` Runs only the router: Feishu handler + routing LLM + node registry. diff --git a/main.py b/main.py deleted file mode 100644 index cfad67b..0000000 --- a/main.py +++ /dev/null @@ -1,117 +0,0 @@ -"""PhoneWork entry point: FastAPI app + Feishu long-connection client.""" - -from __future__ import annotations - -import asyncio -import logging -import time - -import uvicorn -from fastapi import FastAPI -from rich.logging import RichHandler - -from agent.manager import manager -from bot.handler import start_websocket_client, get_ws_status - -logging.basicConfig( - level=logging.DEBUG, - format="%(name)-20s %(message)s", - datefmt="[%X]", - handlers=[RichHandler( - rich_tracebacks=True, - markup=True, - show_path=False, - omit_repeated_times=False, - )], -) -for _noisy in ("httpcore", "httpx", "openai._base_client", "urllib3", "lark_oapi", "websockets"): - logging.getLogger(_noisy).setLevel(logging.WARNING) -logger = logging.getLogger(__name__) - -app = FastAPI(title="PhoneWork", version="0.1.0") - -START_TIME = time.time() - - -@app.get("/health") -async def health() -> dict: - sessions = manager.list_sessions() - ws_status = get_ws_status() - uptime = time.time() - START_TIME - - result = { - "status": "ok", - "uptime_seconds": round(uptime, 1), - "active_sessions": len(sessions), - "websocket": ws_status, - } - - if ws_status.get("connected"): - result["status"] = "ok" - else: - result["status"] = "degraded" - - return result - - -@app.get("/health/claude") -async def health_claude() -> dict: - """Smoke test: run a simple claude -p command.""" - from agent.pty_process import run_claude - import tempfile - import os - - start = time.time() - try: - with tempfile.TemporaryDirectory() as tmpdir: - output = await run_claude( - "Say 'pong' and nothing else", - cwd=tmpdir, - timeout=30.0, - ) - elapsed = time.time() - start - return { - "status": "ok", - "elapsed_seconds": round(elapsed, 2), - "output_preview": output[:100] if output else None, - } - except asyncio.TimeoutError: - return {"status": "timeout", "elapsed_seconds": 30.0} - except Exception as e: - return {"status": "error", "error": str(e)} - - -@app.get("/sessions") -async def list_sessions() -> list: - return manager.list_sessions() - - -@app.on_event("startup") -async def startup_event() -> None: - await manager.start() - from agent.scheduler import scheduler - await scheduler.start() - loop = asyncio.get_running_loop() - start_websocket_client(loop) - logger.info("PhoneWork started") - - -@app.on_event("shutdown") -async def shutdown_event() -> None: - await manager.stop() - from agent.scheduler import scheduler - await scheduler.stop() - logger.info("PhoneWork shut down") - - -if __name__ == "__main__": - from config import PORT - uvicorn.run( - "main:app", - host="0.0.0.0", - port=PORT, - reload=False, - log_level="info", - ws_ping_interval=20, - ws_ping_timeout=60, - ) diff --git a/router/main.py b/router/main.py index 57527a9..56934b6 100644 --- a/router/main.py +++ b/router/main.py @@ -74,3 +74,18 @@ def create_app(router_secret: Optional[str] = None) -> FastAPI: logger.info("Router shut down") return app + + +if __name__ == "__main__": + from config import PORT, ROUTER_SECRET + import uvicorn + app = create_app(router_secret=ROUTER_SECRET) + uvicorn.run( + "router.main:app", + host="0.0.0.0", + port=PORT, + reload=False, + log_level="info", + ws_ping_interval=20, + ws_ping_timeout=60, + ) diff --git a/test_websocket.py b/test_websocket.py new file mode 100644 index 0000000..6f105b3 --- /dev/null +++ b/test_websocket.py @@ -0,0 +1,38 @@ +# test_websocket.py +import asyncio +import websockets + +async def test_connection(): + uri = "ws://47.101.162.164:9600/ws/node" + headers = { + "Authorization": "Bearer family-member-x9" + } + + try: + print(f"Connecting to {uri}...") + async with websockets.connect(uri, extra_headers=headers) as ws: + print("✅ Connection successful!") + print("WebSocket connection established.") + + # 发送注册消息 + import json + register_msg = { + "type": "register", + "node_id": "test-node", + "display_name": "Test Node", + "serves_users": ["test-user"], + "working_dir": "/tmp", + "capabilities": ["claude_code", "shell", "file_ops"] + } + await ws.send(json.dumps(register_msg)) + print("Sent registration message") + + # 等待响应 + response = await ws.recv() + print(f"Received response: {response}") + + except Exception as e: + print(f"❌ Connection failed: {e}") + +if __name__ == "__main__": + asyncio.run(test_connection()) \ No newline at end of file