feat: 实现多主机架构的核心组件

新增路由器、主机客户端和共享协议模块,支持多主机部署模式:
- 路由器作为中央节点管理主机连接和消息路由
- 主机客户端作为工作节点运行本地代理
- 共享协议定义通信消息格式
- 新增独立运行模式standalone.py
- 更新配置系统支持路由模式
This commit is contained in:
Yuyao Huang (Sam)
2026-03-28 14:08:47 +08:00
parent 8ecc701d5e
commit 64297e5e27
17 changed files with 1338 additions and 6 deletions
+37
View File
@@ -67,6 +67,8 @@ async def handle_command(user_id: str, text: str) -> Optional[str]:
return await _cmd_shell(args)
elif cmd == "/remind":
return await _cmd_remind(args)
elif cmd in ("/nodes", "/node"):
return await _cmd_nodes(user_id, args)
else:
return None
@@ -278,6 +280,39 @@ async def _cmd_remind(args: str) -> str:
return f"⏰ Reminder #{job_id} set for {value}{unit} from now"
async def _cmd_nodes(user_id: str, args: str) -> str:
"""List nodes or switch active node."""
from config import ROUTER_MODE
if not ROUTER_MODE:
return "Not in router mode. Run standalone.py for multi-host support."
from router.nodes import get_node_registry
registry = get_node_registry()
if args:
args = args.strip()
if registry.set_active_node(user_id, args):
return f"✓ Active node set to: {args}"
return f"Error: Node '{args}' not found"
nodes = registry.list_nodes()
if not nodes:
return "No nodes connected."
active_node_id = None
active_node = registry.get_active_node(user_id)
if active_node:
active_node_id = active_node.node_id
lines = ["**Connected Nodes:**\n"]
for n in nodes:
marker = "" if n["node_id"] == active_node_id else " "
status = "🟢" if n["status"] == "online" else "🔴"
lines.append(f"{marker}{n['display_name']} {status} sessions={n['sessions']}")
lines.append("\nUse `/node <name>` to switch active node.")
return "\n".join(lines)
def _cmd_help() -> str:
"""Show help."""
return """**Commands:**
@@ -290,5 +325,7 @@ def _cmd_help() -> str:
/shell <cmd> - Run shell command (bypasses LLM)
/remind <time> <msg> - Set reminder (e.g. /remind 10m check build)
/tasks - List background tasks
/nodes - List connected host nodes
/node <name> - Switch active node
/retry - Retry last message
/help - Show this help"""
+42 -4
View File
@@ -86,7 +86,7 @@ def _handle_message(data: P2ImMessageReceiveV1) -> None:
async def _process_message(user_id: str, chat_id: str, text: str) -> None:
"""Process message: check allowlist, then commands, then agent."""
"""Process message: check allowlist, then commands, then route to node or local agent."""
try:
set_current_chat(chat_id)
@@ -96,10 +96,48 @@ async def _process_message(user_id: str, chat_id: str, text: str) -> None:
return
reply = await handle_command(user_id, text)
if reply is None:
if reply is not None:
if reply:
await send_text(chat_id, "chat_id", reply)
return
from config import ROUTER_MODE
if ROUTER_MODE:
from router.routing_agent import route
from router.rpc import forward
from router.nodes import get_node_registry
node_id, reason = await route(user_id, chat_id, text)
if node_id is None:
await send_text(chat_id, "chat_id", f"No host available: {reason}")
return
if node_id == "meta":
registry = get_node_registry()
nodes = registry.list_nodes()
if nodes:
lines = ["Connected Nodes:"]
for n in nodes:
marker = "" if n.get("node_id") == registry.get_active_node(user_id) else " "
lines.append(f"{marker}{n['display_name']} sessions={n['sessions']} {n['status']}")
lines.append("\nUse \"/node <name>\" to switch active node.")
await send_text(chat_id, "chat_id", "\n".join(lines))
else:
await send_text(chat_id, "chat_id", "No nodes connected.")
return
try:
reply = await forward(node_id, user_id, chat_id, text)
if reply:
await send_text(chat_id, "chat_id", reply)
except Exception as e:
logger.exception("Failed to forward to node %s", node_id)
await send_text(chat_id, "chat_id", f"Error communicating with node: {e}")
else:
reply = await agent.run(user_id, text)
if reply:
await send_text(chat_id, "chat_id", reply)
if reply:
await send_text(chat_id, "chat_id", reply)
except Exception:
logger.exception("Error processing message for user %s", user_id)