refactor(router): 重构项目结构,将主入口移至router/main.py
删除旧的main.py文件,将FastAPI应用创建逻辑集中在router/main.py中 添加直接运行router/main.py的支持 更新README.md以反映新的项目结构和使用方式 添加websocket连接测试脚本test_websocket.py
This commit is contained in:
parent
71e3f14788
commit
9173aa6094
18
README.md
18
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.
|
||||
|
||||
|
||||
117
main.py
117
main.py
@ -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,
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
|
||||
38
test_websocket.py
Normal file
38
test_websocket.py
Normal file
@ -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())
|
||||
Loading…
x
Reference in New Issue
Block a user