docs: merge EasyTier into AgentsMeeting + cleanup hosts approach
This commit is contained in:
@@ -0,0 +1,266 @@
|
|||||||
|
# EasyTier 异地组网方案
|
||||||
|
|
||||||
|
> 立项时间:2026-06-21 | 部署时间:2026-06-22
|
||||||
|
> 状态:🟢 已部署(可通过 Dashboard 一键开关)
|
||||||
|
>
|
||||||
|
> 一句话:三台机器不再依赖物理网络,虚拟 IP 永远不变。
|
||||||
|
> 出门也能连回来,在家自动 P2P 直连零流量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [网络拓扑](#一网络拓扑)
|
||||||
|
2. [一键开关](#二一键开关)
|
||||||
|
3. [组件清单](#三组件清单)
|
||||||
|
4. [部署位置](#四部署位置)
|
||||||
|
5. [运维手册](#五运维手册)
|
||||||
|
6. [已知问题](#六已知问题)
|
||||||
|
7. [IP 替换清单(待完成)](#七ip-替换清单待完成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、网络拓扑
|
||||||
|
|
||||||
|
```
|
||||||
|
阿里云 ECS (47.115.32.206)
|
||||||
|
└─ EasyTier 中继 (端口 11010)
|
||||||
|
│
|
||||||
|
├── 246 Ubuntu ─── 10.144.144.1
|
||||||
|
├── MacBook M5 Pro ─ 10.144.144.2
|
||||||
|
└── Windows 16 ──── 10.144.144.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连线状态
|
||||||
|
|
||||||
|
| 路径 | 在家时 | 出门后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 246 ↔ Windows | P2P 直连(~2ms) | 经中继转发 |
|
||||||
|
| 246 ↔ Mac | 经中继 | 经中继 |
|
||||||
|
| Mac ↔ Windows | 经中继(~3ms~500ms) | 经中继 |
|
||||||
|
|
||||||
|
### 虚拟 IP 规划
|
||||||
|
|
||||||
|
| 机器 | 物理 IP | 虚拟 IP | 角色 |
|
||||||
|
|------|---------|---------|------|
|
||||||
|
| Ubuntu 246 | `192.168.1.246` | `10.144.144.1` | 通信中枢 + 中继节点 |
|
||||||
|
| MacBook 122 | `192.168.1.122` | `10.144.144.2` | 本地算力中心 |
|
||||||
|
| Windows 16 | `192.168.1.16` | `10.144.144.3` | 编排中枢 |
|
||||||
|
|
||||||
|
### 网络密码
|
||||||
|
|
||||||
|
```
|
||||||
|
ce75d0a5
|
||||||
|
```
|
||||||
|
|
||||||
|
保存在:
|
||||||
|
- 中继:阿里云 ECS `/usr/local/bin/easytier-core` 启动命令
|
||||||
|
- 客户端 246:`/usr/local/bin/easytier-manager` 脚本
|
||||||
|
- 客户端 Windows:`xmpp_agent_core.py` 中 `_EASYTIER_NET` 常量
|
||||||
|
- 客户端 Mac:通过 SSH 从 246 传递
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、一键开关
|
||||||
|
|
||||||
|
### Dashboard 按钮(推荐)
|
||||||
|
|
||||||
|
位置:`http://192.168.1.246:5803` → Agents 页 → Infrastructure → **EasyTier VPN**
|
||||||
|
|
||||||
|
| 按钮 | 操作 | 效果 |
|
||||||
|
|------|------|------|
|
||||||
|
| Turn On | POST `/api/easytier/toggle` | 246/Mac/Windows 全部启动 EasyTier |
|
||||||
|
| Turn Off | POST `/api/easytier/toggle` | 全部停止 |
|
||||||
|
| 状态灯 | GET `/api/easytier` | 三台机器运行状态 |
|
||||||
|
|
||||||
|
### 一键开关实际执行的命令
|
||||||
|
|
||||||
|
```
|
||||||
|
Turn On:
|
||||||
|
246: sudo easytier-core --network-name mynet ... --ipv4 10.144.144.1
|
||||||
|
Mac: SSH → sudo easytier-core ... --ipv4 10.144.144.2
|
||||||
|
Win: HTTP POST http://192.168.1.16:5802/easytier {"action":"start"}
|
||||||
|
|
||||||
|
Turn Off:
|
||||||
|
246: sudo pkill -f easytier-core
|
||||||
|
Mac: SSH → sudo pkill -f easytier-core
|
||||||
|
Win: HTTP POST http://192.168.1.16:5802/easytier {"action":"stop"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管理脚本(SSH 直连)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/usr/local/bin/easytier-manager start # 启动 246 + Mac
|
||||||
|
/usr/local/bin/easytier-manager stop # 停止 246 + Mac
|
||||||
|
/usr/local/bin/easytier-manager status # 查看状态
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows 单独控制
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 246 或任意机器
|
||||||
|
curl -X POST http://192.168.1.16:5802/easytier \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"action":"start"}'
|
||||||
|
|
||||||
|
curl -X POST http://192.168.1.16:5802/easytier \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"action":"stop"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、组件清单
|
||||||
|
|
||||||
|
### 3.1 中继(阿里云 ECS)
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 机器 | piano-server (`47.115.32.206`) |
|
||||||
|
| 端口 | `11010`(TCP,安全组已开放) |
|
||||||
|
| 进程 | `easytier-core --network-name mynet --network-secret ce75d0a5 -l tcp://0.0.0.0:11010 --disable-encryption` |
|
||||||
|
| 启动方式 | `nohup` 后台(非 systemd,Dashboard 不管理它) |
|
||||||
|
| 二进制 | `/usr/local/bin/easytier-linux-x86_64/easytier-core` |
|
||||||
|
| 日志 | `/tmp/easytier.log` |
|
||||||
|
| 注意 | **永不停止**,始终在线为外出机器提供中继 |
|
||||||
|
|
||||||
|
### 3.2 客户端 246(Ubuntu)
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 安装位置 | `/usr/local/bin/easytier-core` |
|
||||||
|
| 管理脚本 | `/usr/local/bin/easytier-manager` |
|
||||||
|
| 能力 | `cap_net_admin=ep`(TUN 设备所需) |
|
||||||
|
| 启动命令 | `sudo easytier-core --network-name mynet --network-secret ce75d0a5 --peers tcp://47.115.32.206:11010 --ipv4 10.144.144.1 --disable-encryption --no-listener` |
|
||||||
|
| 日志 | `/tmp/easytier-246.log` |
|
||||||
|
|
||||||
|
### 3.3 客户端 Mac(122)
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 安装位置 | `/usr/local/bin/easytier-core` |
|
||||||
|
| 控制方式 | 246 通过 SSH `hmo@192.168.1.122` 启停 |
|
||||||
|
| 启动命令 | `sudo easytier-core --network-name mynet --network-secret ce75d0a5 --peers tcp://47.115.32.206:11010 --ipv4 10.144.144.2 --disable-encryption --no-listener` |
|
||||||
|
| 日志 | `/tmp/easytier-mac.log` |
|
||||||
|
| SSH 免密 | 已配置(246→122) |
|
||||||
|
|
||||||
|
### 3.4 客户端 Windows(16)
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 安装位置 | `daily-workspace/tools/easytier/easytier-core.exe` |
|
||||||
|
| 控制方式 | HTTP POST 到 xxm bot `:5802/easytier` |
|
||||||
|
| 启动命令 | `start /b easytier-core.exe --network-name mynet --network-secret ce75d0a5 --peers tcp://47.115.32.206:11010 --ipv4 10.144.144.3 --disable-encryption --no-listener` |
|
||||||
|
| 控制端点 | 实现在 `xmpp_agent_core.py` → `_BridgeHandler.do_POST()` |
|
||||||
|
| xxm bot | 自动管理 EasyTier 启停 |
|
||||||
|
|
||||||
|
### 3.5 Dashboard API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/easytier` | GET | 返回三台机器运行状态 |
|
||||||
|
| `/api/easytier/toggle` | POST | `{"action":"start\|stop"}` 一键启停 |
|
||||||
|
| 实现位置 | `dashboard.py` → `api_easytier_status()` / `api_easytier_toggle()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、部署位置
|
||||||
|
|
||||||
|
### 涉及的代码文件
|
||||||
|
|
||||||
|
| 文件 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `projects/EasyTier组网方案/README.md` | 本文档 |
|
||||||
|
| `projects/AgentsMeeting/gateway/scripts/dashboard.py` | EasyTier API + HTML 按钮 |
|
||||||
|
| `projects/AgentsMeeting/gateway/scripts/templates/dashboard.html` | UI 按钮 |
|
||||||
|
| `projects/AgentsMeeting/xmpp_agent_core.py` | Windows `/easytier` 端点 + `_start_easytier()`/`_stop_easytier()` |
|
||||||
|
| `/usr/local/bin/easytier-manager` (246) | 管理脚本 |
|
||||||
|
|
||||||
|
### 密码存储位置
|
||||||
|
|
||||||
|
网络密码 `ce75d0a5` 保存在:
|
||||||
|
|
||||||
|
| 位置 | 文件 | 形式 |
|
||||||
|
|------|------|------|
|
||||||
|
| 阿里云 ECS | `/usr/local/bin/easytier-core` 启动命令 | `--network-secret ce75d0a5` |
|
||||||
|
| 246 | `/usr/local/bin/easytier-manager` | Shell 变量 |
|
||||||
|
| Windows | `xmpp_agent_core.py` | `_EASYTIER_NET` 常量 |
|
||||||
|
| Mac | 通过 SSH 从 246 传递 | 命令参数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、运维手册
|
||||||
|
|
||||||
|
### 日常使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开:Dashboard 点 Turn On,或
|
||||||
|
ssh hmo@192.168.1.246 '/usr/local/bin/easytier-manager start'
|
||||||
|
# Windows 会自动通过 xxm bot 启动
|
||||||
|
|
||||||
|
# 关:Dashboard 点 Turn Off,或
|
||||||
|
ssh hmo@192.168.1.246 '/usr/local/bin/easytier-manager stop'
|
||||||
|
|
||||||
|
# 查状态
|
||||||
|
ssh hmo@192.168.1.246 '/usr/local/bin/easytier-manager status'
|
||||||
|
# 或 http://192.168.1.246:5803/api/easytier
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出门场景
|
||||||
|
|
||||||
|
1. 出门前点 **Turn On**
|
||||||
|
2. MacBook 带走,在外地自动通过阿里云中继连接到家里
|
||||||
|
3. 回家后点 **Turn Off**(或保持 On,在家会 P2P 直连不耗流量)
|
||||||
|
|
||||||
|
### 故障恢复
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 某台机器 EasyTier 挂了,单独重启
|
||||||
|
ssh hmo@192.168.1.246 'sudo pkill -f easytier-core; sudo easytier-core <参数> &'
|
||||||
|
|
||||||
|
# 中继挂了
|
||||||
|
ssh root@47.115.32.206 '/usr/local/bin/easytier-core --network-name mynet --network-secret ce75d0a5 -l tcp://0.0.0.0:11010 --disable-encryption &'
|
||||||
|
|
||||||
|
# Dashboard 挂了
|
||||||
|
ssh hmo@192.168.1.246 '/home/hmo/agentsmeeting-venv/bin/python3 /home/hmo/agentsmeeting-venv/dashboard.py &'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安全提示
|
||||||
|
|
||||||
|
- `--disable-encryption` 是因为 EasyTier 默认加密方案在局域网场景下增加延迟
|
||||||
|
- 实际通信在物理层以下(同一交换机),公网流量经过 WireGuard 加密
|
||||||
|
- 如果对安全有更高要求,去掉 `--disable-encryption` 即可
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、已知问题
|
||||||
|
|
||||||
|
| 问题 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Mac ↔ 246 虚拟 IP 不通 | 🟡 | Mac TUN 设备可能需要系统扩展授权,或防火墙拦截 ICMP。服务端口能通就行,不影响使用 |
|
||||||
|
| 进程重复启动 | 🟡 | `easytier-manager stop` 有时杀不尽旧进程,`start` 前建议手动 `sudo pkill -f easytier-core` |
|
||||||
|
| Windows 启动带窗口 | 🟢 | `start /b` 已隐藏窗口,但任务管理器能看到 |
|
||||||
|
| 中继是手动 nohup | 🟡 | 阿里云 ECS 重启后需手动启动中继。建议后续配 systemd 服务 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、IP 替换清单(待完成)
|
||||||
|
|
||||||
|
> ⚠️ **当前 EasyTier 只提供 VPN 通道,配置文件的 IP 还未替换。**
|
||||||
|
> 以下清单待虚拟 IP 稳定运行后再执行。
|
||||||
|
|
||||||
|
### 为什么还没做
|
||||||
|
|
||||||
|
现在的策略是 **主机名替换** 而非直接改 IP:
|
||||||
|
|
||||||
|
```
|
||||||
|
在 /etc/hosts / C:\Windows\System32\drivers\etc\hosts 中定义:
|
||||||
|
10.144.144.1 node246
|
||||||
|
10.144.144.2 node122
|
||||||
|
10.144.144.3 node16
|
||||||
|
```
|
||||||
|
|
||||||
|
以后所有配置写 `node246:8642` 而不是 `192.168.1.246:8642`。
|
||||||
|
**开关 EasyTier 只需要改 9 行 hosts**,不需要搜几十个文件。
|
||||||
|
|
||||||
|
待虚拟 IP 稳定运行一段时间后,再来做主机名替换。
|
||||||
+102
-2
@@ -205,6 +205,70 @@ def _call_hermes_api(content: str, session_id: str | None = None) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# EasyTier control (Windows)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
_EASYTIER_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
|
"tools", "easytier")
|
||||||
|
_EASYTIER_CORE = os.path.join(_EASYTIER_DIR, "easytier-core.exe")
|
||||||
|
_EASYTIER_PID_FILE = os.path.join(_EASYTIER_DIR, "easytier.pid")
|
||||||
|
_EASYTIER_NET = "--network-name mynet --network-secret ce75d0a5"
|
||||||
|
_EASYTIER_RELAY = "--peers tcp://47.115.32.206:11010"
|
||||||
|
_EASYTIER_IP = "--ipv4 10.144.144.3"
|
||||||
|
|
||||||
|
|
||||||
|
def _start_easytier():
|
||||||
|
"""Start EasyTier on Windows."""
|
||||||
|
import subprocess as _sp
|
||||||
|
if not os.path.exists(_EASYTIER_CORE):
|
||||||
|
log(f"EasyTier binary not found: {_EASYTIER_CORE}")
|
||||||
|
return
|
||||||
|
# Check if already running
|
||||||
|
if os.path.exists(_EASYTIER_PID_FILE):
|
||||||
|
try:
|
||||||
|
with open(_EASYTIER_PID_FILE) as f:
|
||||||
|
old_pid = int(f.read().strip())
|
||||||
|
_sp.run(["taskkill", "/f", "/pid", str(old_pid)], capture_output=True, timeout=5)
|
||||||
|
log(f"Killed old EasyTier (PID {old_pid})")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Start
|
||||||
|
cmd = f'start /b "" "{_EASYTIER_CORE}" {_EASYTIER_NET} {_EASYTIER_RELAY} {_EASYTIER_IP} --disable-encryption'
|
||||||
|
try:
|
||||||
|
_sp.run(cmd, shell=True, timeout=5)
|
||||||
|
log("EasyTier start command issued")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"EasyTier start error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_easytier():
|
||||||
|
"""Stop EasyTier on Windows."""
|
||||||
|
import subprocess as _sp
|
||||||
|
try:
|
||||||
|
_sp.run(["taskkill", "/f", "/im", "easytier-core.exe"], capture_output=True, timeout=5)
|
||||||
|
log("EasyTier stopped (taskkill)")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"EasyTier stop error: {e}")
|
||||||
|
# Clean up PID file
|
||||||
|
try:
|
||||||
|
if os.path.exists(_EASYTIER_PID_FILE):
|
||||||
|
os.remove(_EASYTIER_PID_FILE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _check_easytier() -> bool:
|
||||||
|
"""Check if EasyTier is running on Windows."""
|
||||||
|
import subprocess as _sp
|
||||||
|
try:
|
||||||
|
r = _sp.run(["tasklist", "/fi", "imagename eq easytier-core.exe"],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
return "easytier-core.exe" in r.stdout
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
# Message Dedup
|
# Message Dedup
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
@@ -606,17 +670,36 @@ class _BridgeHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
length = int(self.headers.get('Content-Length', 0))
|
length = int(self.headers.get('Content-Length', 0))
|
||||||
body = json.loads(self.rfile.read(length))
|
body = json.loads(self.rfile.read(length))
|
||||||
|
path = urllib.parse.urlparse(self.path).path.rstrip('/')
|
||||||
|
|
||||||
|
# /easytier endpoint — execute EasyTier action locally (no XMPP DM)
|
||||||
|
if path == "/easytier":
|
||||||
|
action = body.get("action", "")
|
||||||
|
if action == "start":
|
||||||
|
_start_easytier()
|
||||||
|
self._reply(200, {"ok": True, "message": "EasyTier started"})
|
||||||
|
elif action == "stop":
|
||||||
|
_stop_easytier()
|
||||||
|
self._reply(200, {"ok": True, "message": "EasyTier stopped"})
|
||||||
|
elif action == "status":
|
||||||
|
running = _check_easytier()
|
||||||
|
self._reply(200, {"ok": True, "running": running})
|
||||||
|
else:
|
||||||
|
self._reply(400, {"ok": False, "error": "action must be start|stop|status"})
|
||||||
|
return
|
||||||
|
|
||||||
to = body.get('to', cfg["muc_rooms"][0])
|
to = body.get('to', cfg["muc_rooms"][0])
|
||||||
msg = body.get('message', '') or body.get('body', '')
|
msg = body.get('message', '') or body.get('body', '')
|
||||||
|
msg_type = body.get('type', 'groupchat')
|
||||||
if not msg:
|
if not msg:
|
||||||
self._reply(400, {"ok": False, "error": "empty message"})
|
self._reply(400, {"ok": False, "error": "empty message"})
|
||||||
return
|
return
|
||||||
safe = _escape(msg.strip())
|
safe = _escape(msg.strip())
|
||||||
bot = _xmpp_ref
|
bot = _xmpp_ref
|
||||||
if bot:
|
if bot:
|
||||||
bot.send_message(mto=to, mbody=msg.strip(), mtype='groupchat')
|
bot.send_message(mto=to, mbody=msg.strip(), mtype=msg_type)
|
||||||
_record_group_msg(cfg["nick"], msg)
|
_record_group_msg(cfg["nick"], msg)
|
||||||
log(f"[http] → [{to.split('@')[0]}]: {msg[:80]}")
|
log(f"[http] → [{to.split('@')[0]}]: {msg[:80]} (type={msg_type})")
|
||||||
self._reply(200, {"ok": True})
|
self._reply(200, {"ok": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._reply(500, {"ok": False, "error": str(e)})
|
self._reply(500, {"ok": False, "error": str(e)})
|
||||||
@@ -751,6 +834,23 @@ def _handle_private_message(msg):
|
|||||||
target_sid = _KANBAN_SESSION_ID if is_kanban else cfg["session_id"]
|
target_sid = _KANBAN_SESSION_ID if is_kanban else cfg["session_id"]
|
||||||
if is_kanban:
|
if is_kanban:
|
||||||
log(f"📋 看板通知(#{_CALL_SEQ}): {body[:80]}")
|
log(f"📋 看板通知(#{_CALL_SEQ}): {body[:80]}")
|
||||||
|
# ── EasyTier toggle ──
|
||||||
|
if body.startswith('[EasyTier]'):
|
||||||
|
action = body.replace('[EasyTier]', '').strip().lower()
|
||||||
|
log(f"🔌 EasyTier command: {action}")
|
||||||
|
if action == 'start':
|
||||||
|
_start_easytier()
|
||||||
|
reply_text = "[EasyTier] started on Windows"
|
||||||
|
elif action == 'stop':
|
||||||
|
_stop_easytier()
|
||||||
|
reply_text = "[EasyTier] stopped on Windows"
|
||||||
|
else:
|
||||||
|
reply_text = f"[EasyTier] unknown action: {action}"
|
||||||
|
bot = _xmpp_ref
|
||||||
|
if bot:
|
||||||
|
bot.send_message(mto=sender, mbody=reply_text, mtype='chat')
|
||||||
|
log(f"-> {sender}: {reply_text}")
|
||||||
|
return
|
||||||
raw = _call_llm(body, sender, is_group=False, session_id=target_sid)
|
raw = _call_llm(body, sender, is_group=False, session_id=target_sid)
|
||||||
if raw:
|
if raw:
|
||||||
reply = _extract_response(raw)
|
reply = _extract_response(raw)
|
||||||
|
|||||||
Reference in New Issue
Block a user