From ef34e6bd1a397ad2e450732a7d50bce747387b2a Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 15 Jul 2026 10:47:39 +0800 Subject: [PATCH] feat: co-located module spec prototype (EasyTier/RDP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - specs/easytier-rdp.json: 双层文档 — human_help(人看) + ai_spec(AI看) - ai_spec 包含: apis, dependencies, architecture, constraints, must_not, tests, related_files - dashboard.py: 新增 /api/module-spec/ 和 /api/module-specs endpoints - dashboard.html: EasyTier/RDP 标题旁加 ? (人看) 和 § (AI看) 按钮 - 点击 ? 显示使用说明(人话), 点击 § 显示 AI spec(机器可读) - CSS: .help-btn 圆形按钮, .spec-modal 全屏弹窗 --- gateway/scripts/dashboard.py | 37 +++++++++++ gateway/scripts/specs/easytier-rdp.json | 81 ++++++++++++++++++++++++ gateway/scripts/templates/dashboard.html | 54 +++++++++++++++- 3 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 gateway/scripts/specs/easytier-rdp.json diff --git a/gateway/scripts/dashboard.py b/gateway/scripts/dashboard.py index 87b1254..4db9721 100644 --- a/gateway/scripts/dashboard.py +++ b/gateway/scripts/dashboard.py @@ -1032,6 +1032,43 @@ def api_rdp_toggle(): return jsonify({"ok": False, "error": str(e)}) +# ════════════════════════════════════════════════════════════ +# Module Spec — 功能模块 co-located 文档 +# ════════════════════════════════════════════════════════════ +_SPECS_DIR = _SCRIPT_DIR / "specs" + +@app.route("/api/module-spec/") +def api_module_spec(module): + """返回指定模块的 co-located spec JSON(人看 + AI 看)""" + spec_file = _SPECS_DIR / f"{module}.json" + if not spec_file.exists(): + return jsonify({"ok": False, "error": f"spec not found: {module}"}) + try: + with open(spec_file, "r", encoding="utf-8") as f: + return jsonify(json.load(f)) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}) + + +@app.route("/api/module-specs") +def api_module_specs(): + """列出所有可用的 module spec""" + specs = [] + if _SPECS_DIR.exists(): + for p in sorted(_SPECS_DIR.glob("*.json")): + try: + with open(p, "r", encoding="utf-8") as f: + data = json.load(f) + specs.append({ + "module": data.get("module", p.stem), + "purpose": data.get("purpose", ""), + "version": data.get("version", ""), + }) + except: + pass + return jsonify({"ok": True, "specs": specs}) + + # ════════════════════════════════════════════════════════════ # Spec 文档展示 # ════════════════════════════════════════════════════════════ diff --git a/gateway/scripts/specs/easytier-rdp.json b/gateway/scripts/specs/easytier-rdp.json new file mode 100644 index 0000000..423e4b6 --- /dev/null +++ b/gateway/scripts/specs/easytier-rdp.json @@ -0,0 +1,81 @@ +{ + "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 subprocess(dashboard 在 Linux,EasyTier/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" + ] + } +} \ No newline at end of file diff --git a/gateway/scripts/templates/dashboard.html b/gateway/scripts/templates/dashboard.html index fda11b1..6c29ed4 100644 --- a/gateway/scripts/templates/dashboard.html +++ b/gateway/scripts/templates/dashboard.html @@ -42,6 +42,21 @@ h1{font-size:32px;font-weight:600;color:var(--accent);margin-bottom:4px}.subtitl .lp{display:none;border-top:1px solid var(--border);padding:12px 16px;background:#0a0e14} .lp.open{display:block}.lh{font-size:18px;color:var(--dim);margin-bottom:8px;display:flex;justify-content:space-between} .lc{font:11px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);max-height:300px;overflow-y:auto;background:#06080c;border:1px solid var(--border);border-radius:6px;padding:10px;white-space:pre-wrap} +.help-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;border:1px solid var(--border);background:var(--card);color:var(--dim);cursor:pointer;font-size:14px;line-height:1;margin-left:8px;flex-shrink:0} +.help-btn:hover{border-color:var(--accent);color:var(--accent)} +.help-btn.ai{color:var(--yellow);border-color:var(--yellow)} +.help-btn.ai:hover{background:var(--yellow);color:#000} +.spec-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);z-index:1000;display:none;align-items:center;justify-content:center} +.spec-modal.show{display:flex} +.spec-modal .modal-box{background:var(--card);border:1px solid var(--border);border-radius:12px;max-width:700px;max-height:80vh;overflow-y:auto;padding:28px;margin:20px} +.spec-modal .modal-box h3{font-size:22px;color:var(--accent);margin-bottom:12px} +.spec-modal .modal-box .close{float:right;cursor:pointer;color:var(--dim);font-size:22px} +.spec-modal .modal-box .close:hover{color:var(--text)} +.spec-modal .modal-box h4{font-size:19px;color:var(--accent);margin:16px 0 6px} +.spec-modal .modal-box p{font-size:18px;color:var(--text);margin:4px 0} +.spec-modal .modal-box ul{margin:4px 0 4px 20px} +.spec-modal .modal-box li{font-size:18px;color:var(--text);margin:2px 0} +.spec-modal .modal-box pre{background:var(--bg);padding:12px;border-radius:6px;font-size:15px;overflow-x:auto;margin:8px 0} .toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:19px;z-index:999;opacity:0;pointer-events:none} .toast.show{opacity:1}.toast.ok{background:#238636;color:#fff}.toast.err{background:#da3633;color:#fff} .ps{margin-top:24px}.ps h2{font-size:18px;color:var(--dim);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)} @@ -66,12 +81,47 @@ th{color:var(--dim);font-weight:600;font-size:19px}

AgentsMeeting

Dashboard
+