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 |
|
| Module | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `standalone.py` | Single-process entry point: runs router + host client together |
|
| `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 |
|
| `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/nodes.py` | Node registry, connection management, user-to-node mapping |
|
||||||
| `router/ws.py` | WebSocket endpoint for host clients, heartbeat, message routing |
|
| `router/ws.py` | WebSocket endpoint for host clients, heartbeat, message routing |
|
||||||
| `router/rpc.py` | Request correlation with asyncio.Future, timeout handling |
|
| `router/rpc.py` | Request correlation with asyncio.Future, timeout handling |
|
||||||
@ -140,13 +139,13 @@ OPENAI_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|||||||
OPENAI_MODEL: glm-4.7
|
OPENAI_MODEL: glm-4.7
|
||||||
|
|
||||||
# Server configuration
|
# 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
|
# Default: 8000
|
||||||
PORT: 8000
|
PORT: 8000
|
||||||
|
|
||||||
# Root directory for all project sessions (absolute path)
|
# Root directory for all project sessions (absolute path)
|
||||||
# Only used in standalone mode (python standalone.py)
|
# 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
|
WORKING_DIR: C:/Users/yourname/projects
|
||||||
|
|
||||||
# Allowlist of Feishu open_ids that may use the bot.
|
# Allowlist of Feishu open_ids that may use the bot.
|
||||||
@ -159,8 +158,7 @@ ALLOWED_OPEN_IDS:
|
|||||||
METASO_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
METASO_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
# Optional: Multi-host mode configuration
|
# Optional: Multi-host mode configuration
|
||||||
# Set ROUTER_MODE to true to enable router mode (deploy on public VPS)
|
# Set ROUTER_SECRET for authentication between router and host clients
|
||||||
ROUTER_MODE: false
|
|
||||||
ROUTER_SECRET: your-shared-secret-for-router-host-auth
|
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):
|
**Router** (public VPS or any reachable machine — runs the Feishu bot):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# keyring.yaml must have ROUTER_MODE: true and ROUTER_SECRET set
|
# keyring.yaml must have ROUTER_SECRET set
|
||||||
python main.py
|
python -m router.main
|
||||||
```
|
```
|
||||||
|
|
||||||
**Host client** (your dev machine behind NAT — runs Claude Code):
|
**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):**
|
**Router Mode (Public VPS):**
|
||||||
```bash
|
```bash
|
||||||
# Set ROUTER_MODE: true in keyring.yaml
|
# Set ROUTER_SECRET in keyring.yaml
|
||||||
python main.py
|
python -m router.main
|
||||||
```
|
```
|
||||||
Runs only the router: Feishu handler + routing LLM + node registry.
|
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")
|
logger.info("Router shut down")
|
||||||
|
|
||||||
return app
|
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