refactor(spec): 拆分 easytier-rdp.json 为 easytier.json + rdp.json

遵守针对性+完整性原则:
- EasyTier section 的 ?/§ 只显示 EasyTier 内容
- RDP section 的 ?/§ 只显示 RDP 内容
- ai_spec.related_models 互相引用对方
- human_help.related 引用对方入口

dashboard.html:
- EasyTier 按钮调用 showModuleHelp('easytier', ...)
- RDP 按钮调用 showModuleHelp('rdp', ...)
- _renderSpecModal 新增 related_modules 显示, 每个 related 模块可点击跳转
This commit is contained in:
hmo
2026-07-15 11:10:28 +08:00
parent ef34e6bd1a
commit 47e323c8fc
4 changed files with 159 additions and 83 deletions
-81
View File
@@ -1,81 +0,0 @@
{
"module": "easytier-rdp",
"version": "1.0",
"purpose": "EasyTier VPN 网络开关 + RDP 远程桌面隧道开关。允许从外网通过 ags.yoin.fun 控制内网 EasyTier 和 RDP 访问。",
"ui_location": "I Tab (Infrastructure) → EasyTier VPN section + RDP Remote Desktop section",
"human_help": {
"title": "EasyTier VPN + RDP 远程桌面",
"description": [
"EasyTier 是一个 P2P VPN 组网工具,用于连接 Windows(192.168.1.16) 和 Linux 246(192.168.1.246)。",
"Turn On/Off 按钮控制两台机器上的 EasyTier 进程。",
"RDP Enable/Disable 按钮控制 SSH 反向隧道,开启后可通过 47.115.32.206:8080 从外网远程桌面到 Windows。"
],
"usage": [
"1. 点击 Turn On 启动 EasyTier VPN(两台机器都启动)",
"2. 点击 Turn Off 关闭 EasyTier VPN",
"3. 点击 Enable 启动 RDP 隧道(需先确保 EasyTier 已连接,否则 SSH 可能不通)",
"4. 点击 Disable 关闭 RDP 隧道",
"5. RDP 连接方式:mstsc /v:47.115.32.206:8080"
],
"troubleshooting": [
"如果 Turn On 返回失败:检查 Windows xmpp_bot (5802) 是否运行",
"如果 RDP Enable 返回失败:检查 SSH 到 246 是否可用(EasyTier VPN 需先连接)",
"如果隧道状态一直 pending:检查 246 的 SSH GatewayPortsyes 是否配置"
]
},
"ai_spec": {
"apis": [
{"method": "GET", "path": "/api/easytier", "returns": "status{windows,246} + virtual_ips{windows,246}", "proxied_to": "xmpp_bot /easytier action=status"},
{"method": "POST", "path": "/api/easytier/toggle", "body": "{action: start|stop}", "returns": "{ok, message}", "proxied_to": "xmpp_bot /easytier action=start|stop"},
{"method": "GET", "path": "/api/rdp", "returns": "rdp_enabled + tunnel_running + public_endpoint", "proxied_to": "xmpp_bot /rdp action=status"},
{"method": "POST", "path": "/api/rdp/toggle", "body": "{action: start|stop}", "returns": "{ok, message}", "proxied_to": "xmpp_bot /rdp action=start|stop"}
],
"dependencies": [
"xmpp_bot on Windows 192.168.1.16:5802 (HTTP bridge with /easytier and /rdp endpoints)",
"XMPP_BRIDGE_URL env var (default: http://192.168.1.16:5802) — set in systemd service",
"_bridge_post() helper in dashboard.py — proxies POST to xmpp_bot with X-Api-Key header",
"_BRIDGE_KEY = 'xxm_bridge_8f3a2c' — API key for xmpp_bot HTTP bridge",
"port_open() helper in dashboard.py — socket-based port check for tunnel status"
],
"architecture": {
"flow": "Dashboard(246:5803) → _bridge_post() → xmpp_bot(Windows:5802) → subprocess/system",
"easytier_mechanism": "xmpp_bot receives POST /easytier {action} → starts/stops easytier-core process on respective machine",
"rdp_mechanism": "xmpp_bot receives POST /rdp {action} → starts/stops SSH reverse tunnel: ssh -R 0.0.0.0:8080:localhost:3389 root@47.115.32.206"
},
"constraints": [
"VPN IP: Windows=10.144.144.3, 246=10.144.144.1 (NOT .2 — .2 was wrong in old code)",
"RDP SSH reverse tunnel: ssh -R 0.0.0.0:8080:localhost:3389 root@47.115.32.206",
"GatewayPorts yes required in /etc/ssh/sshd_config on 47.115.32.206 (Aliyun)",
"xmpp_bot must be running on Windows with /rdp and /easytier HTTP endpoints (API key: xxm_bridge_8f3a2c)",
"dashboard.py runs on 246 (Linux), xmpp_bot runs on Windows — they communicate over LAN HTTP"
],
"tests": [
{"id": "ET01", "name": "EasyTier toggle start returns ok", "endpoint": "POST /api/easytier/toggle {action:start}"},
{"id": "ET02", "name": "EasyTier toggle stop returns ok", "endpoint": "POST /api/easytier/toggle {action:stop}"},
{"id": "RDP01", "name": "RDP toggle start returns ok", "endpoint": "POST /api/rdp/toggle {action:start}"},
{"id": "RDP02", "name": "RDP toggle stop returns ok", "endpoint": "POST /api/rdp/toggle {action:stop}"},
{"id": "ET-RDP01", "name": "EasyTier status returns virtual_ips", "endpoint": "GET /api/easytier"},
{"id": "ET-RDP02", "name": "RDP status returns public_endpoint 47.115.32.206:8080", "endpoint": "GET /api/rdp"}
],
"must_not": [
"不要把 proxy 调用改成 local subprocessdashboard 在 LinuxEasyTier/RDP 在 Windows",
"不要重写整个 fI() 函数来改 EasyTier/RDP 部分 — 用 create-once/update-state pattern",
"不要把 VPN IP 从 .3 改成 .2 — .3 是正确的",
"不要删除 _bridge_post() helper 或 _BRIDGE_KEY — 它们是 proxy 的核心",
"不要在 fI() 里用 fill('infra',h) — 这会清空整个 ct-infra 导致闪烁"
],
"known_issues": [
"如果 xmpp_bot (5802) 停了,所有 EasyTier/RDP 操作都会失败 — 检查 Windows pythonw.exe 进程",
"如果 246 上 SSH 到 Windows 不通,RDP 隧道无法启动 — 先确保 EasyTier VPN 连接",
"EasyTier status 246 字段可能显示 unknown — 这是正常的,因为 246 的 EasyTier 状态不一定通过 xmpp_bot 返回"
],
"related_files": [
"gateway/scripts/dashboard.py — /api/easytier*, /api/rdp*, _bridge_post(), port_open()",
"gateway/scripts/templates/dashboard.html — fI() function creates ET/RDP sections",
"xmpp_agent_core.py — /easytier and /rdp HTTP endpoints (lines ~780, ~794)",
"/etc/systemd/system/agentsmeeting-dashboard.service — XMPP_BRIDGE_URL env var"
]
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"module": "easytier",
"version": "1.0",
"purpose": "EasyTier VPN 网络开关。控制 Windows(192.168.1.16) 和 Linux 246(192.168.1.246) 上的 EasyTier VPN 进程,让两台机器组成 P2P 内网。",
"ui_location": "I Tab (Infrastructure) → EasyTier VPN section",
"human_help": {
"title": "EasyTier VPN",
"description": [
"EasyTier 是一个 P2P VPN 组网工具,用于连接 Windows(192.168.1.16) 和 Linux 246(192.168.1.246) 两台机器。",
"开启后两台机器通过虚拟网卡通信:Windows 获得 10.144.144.3246 获得 10.144.144.1。",
"RDP 远程桌面隧道依赖 EasyTier VPN 先连通,请先确保这里显示 Connected 再操作 RDP。"
],
"usage": [
"1. 点击 Turn On 启动 EasyTier VPN(两台机器都启动)",
"2. 点击 Turn Off 关闭 EasyTier VPN",
"3. 状态显示 Connected 表示两台机器都已加入 VPN 网络"
],
"troubleshooting": [
"如果 Turn On 返回失败:检查 Windows xmpp_bot (pythonw.exe, port 5802) 是否运行",
"如果状态显示 Off 但实际已启动:等待 1-2 秒再刷新,或检查 246 上 easytier-core 进程"
],
"related": "RDP Remote Desktop 依赖本 VPN 通道,参见 RDP section 的帮助"
},
"ai_spec": {
"apis": [
{"method": "GET", "path": "/api/easytier", "returns": "status{windows,246} + virtual_ips{windows,246}", "proxied_to": "xmpp_bot /easytier action=status"},
{"method": "POST", "path": "/api/easytier/toggle", "body": "{action: start|stop}", "returns": "{ok, message}", "proxied_to": "xmpp_bot /easytier action=start|stop"}
],
"dependencies": [
"xmpp_bot on Windows 192.168.1.16:5802 (HTTP bridge with /easytier endpoint)",
"XMPP_BRIDGE_URL env var (default: http://192.168.1.16:5802) — set in systemd service",
"_bridge_post() helper in dashboard.py — proxies POST to xmpp_bot with X-Api-Key header",
"_BRIDGE_KEY = 'xxm_bridge_8f3a2c' — API key for xmpp_bot HTTP bridge"
],
"architecture": {
"flow": "Dashboard(246:5803) → _bridge_post() → xmpp_bot(Windows:5802) → starts/stops easytier-core process on respective machine",
"mechanism": "xmpp_bot receives POST /easytier {action} → starts/stops easytier-core process"
},
"constraints": [
"VPN IP: Windows=10.144.144.3, 246=10.144.144.1 (NOT .2 — .2 was wrong in old code, 已修正)",
"xmpp_bot must be running on Windows with /easytier HTTP endpoint (API key: xxm_bridge_8f3a2c)",
"dashboard.py runs on 246 (Linux), xmpp_bot runs on Windows — 通信走 LAN HTTP,不是 local subprocess"
],
"must_not": [
"不要把 proxy 调用改成 local subprocessdashboard 在 LinuxEasyTier 在 Windows",
"不要把 VPN IP 从 .3 改成 .2 — .3 是正确的 Windows IP",
"不要删除 _bridge_post() helper 或 _BRIDGE_KEY — 它们是 proxy 的核心",
"不要在 fI() 里用 fill('infra',h) — 这会清空整个 ct-infra 区域导致 EasyTier/RDP 元素闪烁"
],
"related_modules": [
{"module": "rdp", "relation": "RDP 远程桌面隧道依赖 EasyTier VPN 连接,启动 RDP 前应确保 EasyTier Connected"}
],
"tests": [
{"id": "ET01", "name": "EasyTier toggle start returns ok", "endpoint": "POST /api/easytier/toggle {action:start}"},
{"id": "ET02", "name": "EasyTier toggle stop returns ok", "endpoint": "POST /api/easytier/toggle {action:stop}"},
{"id": "ET03", "name": "EasyTier status returns virtual_ips with windows=10.144.144.3", "endpoint": "GET /api/easytier"}
],
"known_issues": [
"如果 xmpp_bot (5802) 停了,EasyTier 操作会失败 — 检查 Windows pythonw.exe 进程",
"EasyTier status 246 字段可能显示 unknown — 因为 246 的 EasyTier 状态不一定通过 xmpp_bot 返回"
],
"related_files": [
"gateway/scripts/dashboard.py — /api/easytier*, _bridge_post()",
"gateway/scripts/templates/dashboard.html — fI() function creates EasyTier section (id=easytier-section)",
"gateway/scripts/specs/easytier.json — 本 spec 文件",
"gateway/scripts/specs/rdp.json — 关联模块 RDP 的 spec",
"xmpp_agent_core.py — /easytier HTTP endpoint (line ~780)"
]
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"module": "rdp",
"version": "1.0",
"purpose": "RDP 远程桌面隧道开关。通过 SSH 反向隧道将 Windows 3389 端口暴露到 Aliyun 公网 IP,实现从外网远程桌面到 Windows。",
"ui_location": "I Tab (Infrastructure) → RDP Remote Desktop section",
"human_help": {
"title": "RDP Remote Desktop",
"description": [
"通过 SSH 反向隧道把 Windows 的 RDP 端口(3389)暴露到 Aliyun 公网服务器(47.115.32.206:8080)。",
"开启后在外网可用 mstsc /v:47.115.32.206:8080 远程桌面到 Windows。",
"本功能依赖 EasyTier VPN 先连通 — SSH 到 246 的连接走 VPN 内网(10.144.144.1)。",
"请先确保 EasyTier VPN 已 Connected,再点击 Enable。"
],
"usage": [
"1. 先在 EasyTier VPN section 点击 Turn On,确认 Connected",
"2. 回到本 section 点击 Enable 启动 SSH 反向隧道",
"3. 在外网电脑运行 mstsc /v:47.115.32.206:8080 连接远程桌面",
"4. 不用时点击 Disable 关闭隧道"
],
"troubleshooting": [
"如果 Enable 返回失败:检查 EasyTier VPN 是否 ConnectedRDP 依赖 VPN 内网 SSH",
"如果隧道状态一直 pending:检查 246 的 /etc/ssh/sshd_config 是否有 GatewayPorts yes",
"如果 mstsc 连不上:检查 Aliyun 安全组是否放行 8080/TCP",
"如果 timeout:从外网执行 Test-NetConnection 47.115.32.206 -Port 8080 看端口是否真的 listening"
],
"related": "依赖 EasyTier VPN 内网通道,参见 EasyTier VPN section 的帮助"
},
"ai_spec": {
"apis": [
{"method": "GET", "path": "/api/rdp", "returns": "rdp_enabled + tunnel_running + public_endpoint(47.115.32.206:8080)", "proxied_to": "xmpp_bot /rdp action=status"},
{"method": "POST", "path": "/api/rdp/toggle", "body": "{action: start|stop}", "returns": "{ok, message}", "proxied_to": "xmpp_bot /rdp action=start|stop"}
],
"dependencies": [
"xmpp_bot on Windows 192.168.1.16:5802 — /rdp HTTP endpoint",
"_bridge_post() + _BRIDGE_KEY in dashboard.py — proxy 机制同 EasyTier",
"port_open() helper in dashboard.py — 检查 SSH 隧道端口 8080 是否监听",
"EasyTier VPN 必须先 Connected — SSH 到 246 走 VPN 内网 10.144.144.1"
],
"architecture": {
"flow": "Dashboard(246:5803) → _bridge_post() → xmpp_bot(Windows:5802) → 启动 SSH reverse tunnel",
"rdp_mechanism": "xmpp_bot receives POST /rdp {action} → 执行 ssh -R 0.0.0.0:8080:localhost:3389 root@47.115.32.206 把 Windows 3389 转发到 Aliyun 公网",
"public_endpoint": "47.115.32.206:8080 (Aliyun 公网)"
},
"constraints": [
"RDP SSH 反向隧道命令: ssh -R 0.0.0.0:8080:localhost:3389 root@47.115.32.206",
"GatewayPorts yes 必须配置在 47.115.32.206 (Aliyun) 的 /etc/ssh/sshd_config — 否则只 listen 127.0.0.1 外网连不进来",
"EasyTier VPN 必须先 Connected — 否则 Windows SSH 到 246 不通,隧道无法建立",
"xmpp_bot 必须运行在 Windows (pythonw.exe, port 5802) — 它是实际执行 SSH 命令的一方",
"RDP 端口固定 3389 (Windows 默认),隧道端口固定 8080"
],
"must_not": [
"不要在 EasyTier VPN 未 Connected 时启 RDP — 会因为 SSH 不通而失败",
"不要用 SSH 端口转发替代 SSH 反向隧道 — 这是 -R (reverse) 不是 -L (local)",
"不要把 public_endpoint 从 47.115.32.206:8080 改成其他 — 这是 Aliyun 公网入口",
"不要把 tunnel_running 判断从 port_open(8080) 改成依赖 xmpp_bot 的 tunnel 字段 — xmpp_bot 不报告隧道状态,只能从 246 主动 probe",
"不要重写整个 fI() 函数 — 用 create-once/update-state pattern 修改 RDP section"
],
"related_modules": [
{"module": "easytier", "relation": "RDP 隧道依赖 EasyTier VPN 内网通道。启动 RDP 前必须确保 EasyTier Connected"}
],
"tests": [
{"id": "RDP01", "name": "RDP toggle start returns ok", "endpoint": "POST /api/rdp/toggle {action:start}"},
{"id": "RDP02", "name": "RDP toggle stop returns ok", "endpoint": "POST /api/rdp/toggle {action:stop}"},
{"id": "RDP03", "name": "RDP status returns public_endpoint 47.115.32.206:8080", "endpoint": "GET /api/rdp"},
{"id": "RDP04", "name": "After enable, port 8080 on Aliyun is listening (Test-NetConnection)", "endpoint": "external TCP check 47.115.32.206:8080"}
],
"known_issues": [
"如果 Enable 后 status 一直 Tunnel pending:检查 /etc/ssh/sshd_config GatewayPorts yes,检查 Aliyun 安全组 8080/TCP",
"如果 mstsc 从外网连不上:检查 Aliyun 安全组 — 需要 8080/TCP 入方向放行",
"如果隧道断了但 status 显示 Connected:这是 stale state — port_open() 会重检 8080 端口下次刷新自动修正"
],
"related_files": [
"gateway/scripts/dashboard.py — /api/rdp*, _bridge_post(), port_open()",
"gateway/scripts/templates/dashboard.html — fI() function creates RDP section (id=rdp-section)",
"gateway/scripts/specs/rdp.json — 本 spec 文件",
"gateway/scripts/specs/easytier.json — 关联模块 EasyTier 的 spec",
"xmpp_agent_core.py — /rdp HTTP endpoint (line ~794)",
"/etc/ssh/sshd_config on 47.115.32.206 — GatewayPorts yes 配置"
]
}
}
+4 -2
View File
@@ -109,6 +109,7 @@ function _renderSpecModal(spec,type){
if(hh.description){h+='<h4>说明</h4><ul>';hh.description.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
if(hh.usage){h+='<h4>使用方法</h4><ul>';hh.usage.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
if(hh.troubleshooting){h+='<h4>常见问题</h4><ul>';hh.troubleshooting.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
if(hh.related){h+='<h4>关联模块</h4><p>'+esc(hh.related)+'</p>';}
}else{
var ai=spec.ai_spec||{};
h+='<h3>§ AI Spec — '+esc(spec.module||'')+'</h3>';
@@ -119,6 +120,7 @@ function _renderSpecModal(spec,type){
if(ai.must_not){h+='<h4>禁止行为</h4><ul>';ai.must_not.forEach(function(d){h+='<li style="color:var(--red)">'+esc(d)+'</li>'});h+='</ul>';}
if(ai.tests){h+='<h4>测试用例</h4><ul>';ai.tests.forEach(function(t){h+='<li><code>'+esc(t.id)+'</code> '+esc(t.name)+'</li>'});h+='</ul>';}
if(ai.related_files){h+='<h4>相关文件</h4><ul>';ai.related_files.forEach(function(f){h+='<li><code>'+esc(f)+'</code></li>'});h+='</ul>';}
if(ai.related_modules&&ai.related_modules.length){h+='<h4>关联模块</h4><ul>';ai.related_modules.forEach(function(m){h+='<li><span class="help-btn" style="cursor:pointer;margin-right:6px" onclick="showModuleHelp(\''+esc(m.module)+'\',\'ai\')">§</span><code>'+esc(m.module)+'</code> — '+esc(m.relation)+'</li>'});h+='</ul>';}
}
box.innerHTML=h;
}
@@ -191,7 +193,7 @@ async function fI(){
var et=document.getElementById('easytier-section');
if(!et){
et=document.createElement('div');et.id='easytier-section';et.className='ps';et.style.marginTop='16px';
et.innerHTML='<h2>EasyTier VPN<span class="help-btn" onclick="showModuleHelp(\'easytier-rdp\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'easytier-rdp\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=et-w></span>Windows <span id=et-w-ip></span></div><div class=i><span class="d" id=et-246></span>246 <span id=et-246-ip></span></div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-et-on>Turn On</button><button class="btn x" id=btn-et-off>Turn Off</button><span id=et-status style="font-size:12px;color:var(--dim)">-</span></div>';
et.innerHTML='<h2>EasyTier VPN<span class="help-btn" onclick="showModuleHelp(\'easytier\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'easytier\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=et-w></span>Windows <span id=et-w-ip></span></div><div class=i><span class="d" id=et-246></span>246 <span id=et-246-ip></span></div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-et-on>Turn On</button><button class="btn x" id=btn-et-off>Turn Off</button><span id=et-status style="font-size:12px;color:var(--dim)">-</span></div>';
ci.appendChild(et);
document.getElementById('btn-et-on').onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){fI()}).catch(function(){toast('ET Fail','err')})};
document.getElementById('btn-et-off').onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){fI()}).catch(function(){toast('ET Fail','err')})};
@@ -210,7 +212,7 @@ async function fI(){
var rd=document.getElementById('rdp-section');
if(!rd){
rd=document.createElement('div');rd.id='rdp-section';rd.className='ps';rd.style.marginTop='16px';
rd.innerHTML='<h2>RDP Remote Desktop<span class="help-btn" onclick="showModuleHelp(\'easytier-rdp\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'easytier-rdp\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=rdp-st></span>RDP <span id=rdp-info></span></div><div class=i><span class="d" id=rdp-tun></span>Tunnel</div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-rdp-on>Enable</button><button class="btn x" id=btn-rdp-off>Disable</button><span id=rdp-status-txt style="font-size:12px;color:var(--dim)">-</span></div>';
rd.innerHTML='<h2>RDP Remote Desktop<span class="help-btn" onclick="showModuleHelp(\'rdp\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'rdp\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=rdp-st></span>RDP <span id=rdp-info></span></div><div class=i><span class="d" id=rdp-tun></span>Tunnel</div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-rdp-on>Enable</button><button class="btn x" id=btn-rdp-off>Disable</button><span id=rdp-status-txt style="font-size:12px;color:var(--dim)">-</span></div>';
ci.appendChild(rd);
document.getElementById('btn-rdp-on').onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){fI()}).catch(function(){toast('RDP Fail','err')})};
document.getElementById('btn-rdp-off').onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){fI()}).catch(function(){toast('RDP Fail','err')})};