EasyTier+RDP恢复 + G规范Tab + dev-spec更新

- EasyTier VPN: /api/easytier + /api/easytier/toggle + dashboard展示
- RDP远程桌面: /api/rdp + /api/rdp/toggle + dashboard展示
- G规范子Tab: /api/spec路由 + dashboard展示完整dev-spec.md
- dev-spec.md: 新增G节(测试验证规范/弹窗铁律/路径规则)
- 修复JS函数名冲突导致页面白屏
- Playwright验证: 0 errors, 所有Tab正常
This commit is contained in:
hmo
2026-07-14 02:49:08 +08:00
parent 5fb04dd2d5
commit 876a42343b
2 changed files with 137 additions and 4 deletions
+129
View File
@@ -812,6 +812,135 @@ def port_open(port):
return False
# ════════════════════════════════════════════════════════════
# EasyTier VPN + RDP 远程桌面控制
# ════════════════════════════════════════════════════════════
@app.route("/api/easytier")
def api_easytier_status():
"""EasyTier VPN 状态"""
try:
r = subprocess.run(["tasklist", "/FI", "IMAGENAME eq easytier-core.exe", "/NH"],
capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
windows_running = "easytier-core" in r.stdout
# Proxy to xmpp_bot for 246 status (via POST /easytier with action=status)
status_246 = "unknown"
try:
req = urllib.request.Request("http://127.0.0.1:5802/easytier",
data=b'{"action":"status"}',
headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"},
method="POST")
resp = urllib.request.urlopen(req, timeout=5)
data = json.loads(resp.read())
status_246 = "running" if data.get("running") else "stopped"
except:
pass
return jsonify({
"ok": True,
"status": {"windows": "running" if windows_running else "stopped", "246": status_246},
"virtual_ips": {"windows": "10.144.144.2", "246": "10.144.144.1"},
"windows_running": windows_running,
})
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/api/easytier/toggle", methods=["POST"])
def api_easytier_toggle():
"""EasyTier VPN 开关"""
try:
body = json.loads(request.data)
action = body.get("action", "")
if action not in ("start", "stop"):
return jsonify({"ok": False, "error": "action must be start|stop"})
# Proxy to xmpp_bot which handles EasyTier via subprocess
req = urllib.request.Request(f"http://127.0.0.1:5802/easytier",
data=json.dumps({"action": action}).encode(),
headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"},
method="POST")
resp = urllib.request.urlopen(req, timeout=15)
data = json.loads(resp.read())
return jsonify({"ok": data.get("ok", False), "message": data.get("message", "")})
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/api/rdp")
def api_rdp_status():
"""RDP 远程桌面状态"""
try:
# Check if RDP port 3389 is accessible
rdp_enabled = False
try:
s = urllib.request.urlopen("http://127.0.0.1:5802/rdp",
data=b'{"action":"status"}',
headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"},
timeout=5)
data = json.loads(s.read())
rdp_enabled = data.get("ok", False)
except:
pass
# Check SSH tunnel (port 8080)
tunnel_running = port_open(8080)
return jsonify({
"ok": True,
"rdp_enabled": rdp_enabled,
"tunnel_running": tunnel_running,
"rdp_port": 3389,
"tunnel_port": 8080,
"public_endpoint": "47.115.32.206:8080",
})
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/api/rdp/toggle", methods=["POST"])
def api_rdp_toggle():
"""RDP 远程桌面开关"""
try:
body = json.loads(request.data)
action = body.get("action", "")
if action not in ("start", "stop"):
return jsonify({"ok": False, "error": "action must be start|stop"})
req = urllib.request.Request(f"http://127.0.0.1:5802/rdp",
data=json.dumps({"action": action}).encode(),
headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"},
method="POST")
resp = urllib.request.urlopen(req, timeout=15)
data = json.loads(resp.read())
return jsonify({"ok": data.get("ok", False), "message": data.get("message", "")})
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
# ════════════════════════════════════════════════════════════
# Spec 文档展示
# ════════════════════════════════════════════════════════════
@app.route("/api/spec")
def api_spec():
"""返回开发规范文档内容"""
# Try multiple possible locations
candidates = [
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))), ".memory", "dev-spec.md"),
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), ".memory", "dev-spec.md"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "..", ".memory", "dev-spec.md"),
]
spec_path = None
for c in candidates:
p = os.path.normpath(c)
if os.path.exists(p):
spec_path = p
break
if not spec_path:
return jsonify({"ok": False, "error": "dev-spec.md not found", "content": ""})
try:
with open(spec_path, "r", encoding="utf-8") as f:
content = f.read()
return jsonify({"ok": True, "content": content, "path": spec_path})
except Exception as e:
return jsonify({"ok": False, "error": str(e), "content": ""})
@app.route("/api/platform")
@app.route("/api/health")
def api_health():
+8 -4
View File
@@ -66,7 +66,7 @@ th{color:var(--dim);font-weight:600;font-size:11px}
<h1>AgentsMeeting</h1><div class="subtitle" id="subtitle">Dashboard</div>
<div class="tabs" id="tabs"></div><div id="panes"></div><div id="toast" class="toast"></div>
<script>
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"}]}];
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"},{"id":"spec","label":"G 规范"}]}];
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'git';
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'<').replace(/>/g,'>')}
function qs(id){return document.getElementById(id)}
@@ -103,8 +103,12 @@ async function fT(){try{var r=await fetch('/api/todos'),d=await r.json();fill('t
async function fE(){try{var r=await fetch('/api/metagrowth'),d=await r.json();fill('meta','fixes: '+(d.total_fixes||0)+' commits: '+(d.commit_count||0))}catch(e){}}
async function fF(){try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}fill('exp',ok+'/'+d.expected.length+' compliant')}catch(e){}}
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json(),n=(d.tasks||[]).length;fill('kanban',n?'<span>'+n+' tasks</span>':'<span class="dim">No tasks</span>')}catch(e){}}
async function fI(){try{var r=await fetch('/api/platform'),d=await r.json();var h='<div class="ps"><h2>Platform</h2><div class="psv">';for(var i=0;i<d.length;i++){h+='<div class="i"><span class="d '+(d[i].status==='running'?'ok':'stopped')+'"></span>'+esc(d[i].name)+'</div>';}h+='</div></div>';try{var r2=await fetch('/api/ejabberd'),e=await r2.json();var ea=e.alive||e.xmpp_bot_connected;h+='<div class="ps"><h2>Ejabberd</h2><div class="psv"><div class="i"><span class="d '+(ea?'ok':'stopped')+'"></span>'+(ea?'ALIVE':'DOWN')+'</div></div></div>';}catch(e2){}fill('infra',h)}catch(e){}}
var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,kanban:fK,infra:fI,principles:function(){return(FETCHES[as]||function(){})()}};
async function fI(){try{var r=await fetch('/api/platform'),d=await r.json();var h='<div class="ps"><h2>Platform</h2><div class="psv">';for(var i=0;i<d.length;i++){h+='<div class="i"><span class="d '+(d[i].status==='running'?'ok':'stopped')+'"></span>'+esc(d[i].name)+'</div>';}h+='</div></div>';try{var r2=await fetch('/api/ejabberd'),e=await r2.json();var ea=e.alive||e.xmpp_bot_connected;h+='<div class="ps"><h2>EasyTier VPN</h2><div class="psv"><div class="i"><span class="d stopped" id="et-w"></span>Windows <span id="et-w-ip"></span></div><div class="i"><span class="d stopped" id="et-246"></span>246 <span id="et-246-ip"></span></div></div></div><div class="ps"><h2>RDP Remote Desktop</h2><div class="psv"><div class="i"><span class="d stopped" id="rdp-st"></span>RDP <span id="rdp-info"></span></div><div class="i"><span class="d stopped" id="rdp-tun"></span>Tunnel</div></div></div><div class="ps"><h2>Ejabberd</h2><div class="psv"><div class="i"><span class="d '+(ea?'ok':'stopped')+'"></span>'+(ea?'ALIVE':'DOWN')+'</div></div></div>';}catch(e2){}fill('infra',h)}catch(e){}}
var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,spec:fS2,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();await Promise.all(Object.keys(FETCHES).map(function(k){return FETCHES[k]()}))}
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);
async function fE2(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var e1=document.getElementById('et-w');if(e1){e1.className='d '+(s.windows==='running'?'ok':'stopped')}var e2=document.getElementById('et-246');if(e2){e2.className='d '+(s['246']==='running'?'ok':'stopped')}if(d.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=d.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=d.virtual_ips['246']||''}}catch(e){}}
async function fR(){try{var r=await fetch('/api/rdp'),d=await r.json();var e1=document.getElementById('rdp-st');if(e1){e1.className='d '+(d.rdp_enabled?'ok':'stopped')}var e2=document.getElementById('rdp-tun');if(e2){e2.className='d '+(d.tunnel_running?'ok':'stopped')}var info=document.getElementById('rdp-info');if(info)info.textContent=d.public_endpoint||''}catch(e){}}
async function fS2(){try{var r=await fetch('/api/spec'),d=await r.json();if(!d.ok||!d.content){fill('spec','Failed');return}fill('spec','<div class=mono style=font-size:11px;color:var(--dim);white-space:pre-wrap;max-height:500px;overflow-y:auto;background:var(--bg);padding:12px;border-radius:6px>'+esc(d.content)+'</div>')}catch(e){fill('spec','Error')}}
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);setInterval(fE,10000);setInterval(fR,10000);
</script></body></html>