release: nexus btpanel session fix and app-v2

This commit is contained in:
Codex Release Bot
2026-07-08 22:31:31 +08:00
commit 1933f0af6e
2457 changed files with 350105 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""Nexus MCP Server — 测试/运维工具集 (mcp 1.x API)
All paths read from NEXUS_DEPLOY_DIR env var (default: /opt/nexus).
"""
import json, os, re, subprocess
from mcp.server.fastmcp import FastMCP
API_BASE = "http://127.0.0.1:8600"
API_KEY = ""
DEPLOY_DIR = os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus")
def _load_db_creds():
"""Load DB credentials from .env file (never hardcode)"""
env_path = os.path.join(DEPLOY_DIR, ".env")
creds = {"user": "nexus", "pass": "", "db": "nexus"}
try:
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
line = line.strip()
if line.startswith("NEXUS_DATABASE_URL="):
url = line.split("=", 1)[1].strip()
m = re.match(r"mysql\+pymysql://([^:]+):([^@]+)@([^/]+)/(.+)", url)
if m:
creds["user"] = m.group(1)
creds["pass"] = m.group(2)
creds["db"] = m.group(4)
except Exception:
pass
return creds
server = FastMCP("nexus")
# ================================================================
# T1: API 调用
# ================================================================
@server.tool()
async def api_call(method: str = "GET", path: str = "/health", body: str = "{}"):
"""调用 Nexus Python API。method: GET/POST/PUT/DELETE, path: /api/servers/等, body: JSON字符串"""
import urllib.request
url = f"{API_BASE}{path}"
data = body.encode() if method in ("POST","PUT") else None
req = urllib.request.Request(url, data=data, method=method)
req.add_header("Content-Type", "application/json")
if API_KEY: req.add_header("X-API-Key", API_KEY)
try:
resp = urllib.request.urlopen(req, timeout=10)
return json.dumps(json.loads(resp.read()), indent=2, ensure_ascii=False)
except Exception as e:
return str(e)
# ================================================================
# T2: 数据库查询 (read-only, credentials from .env)
# ================================================================
@server.tool()
async def db_query(sql: str = "SELECT COUNT(*) FROM servers"):
"""直查 MySQL nexus 数据库。只读查询"""
sql_upper = sql.strip().upper()
if not sql_upper.startswith("SELECT") and not sql_upper.startswith("SHOW") and not sql_upper.startswith("DESCRIBE"):
return "Error: Only SELECT/SHOW/DESCRIBE queries allowed"
if ";" in sql.rstrip(";").strip() and len(sql.rstrip(";").strip().split(";")) > 1:
return "Error: Multiple statements not allowed"
try:
creds = _load_db_creds()
p = subprocess.run(
['mysql', f'-u{creds["user"]}', f'-p{creds["pass"]}', creds["db"], '-e', sql],
capture_output=True, text=True, timeout=10
)
return p.stdout if p.returncode == 0 else p.stderr
except Exception as e:
return str(e)
# ================================================================
# T3: 服务管理
# ================================================================
@server.tool()
async def service(action: str = "status"):
"""管理 Python 后端服务。action: status/start/stop/restart"""
try:
p = subprocess.run(['supervisorctl', action, 'nexus'], capture_output=True, text=True, timeout=15)
return p.stdout or p.stderr or f"nexus {action}: OK"
except Exception as e:
return str(e)
# ================================================================
# T4: 日志查看
# ================================================================
@server.tool()
async def logs(lines: int = 50):
"""查看 Python 后端日志。lines: 行数"""
try:
log_path = os.path.join(DEPLOY_DIR, "logs", "nexus.log")
p = subprocess.run(['tail', '-n', str(lines), log_path],
capture_output=True, text=True, timeout=10)
return p.stdout[-3000:]
except Exception as e:
return str(e)
# ================================================================
# T5: 运行测试
# ================================================================
@server.tool()
async def run_test():
"""运行 test_api.py 端到端测试"""
try:
p = subprocess.run(['python3', os.path.join(DEPLOY_DIR, 'tests', 'test_api.py')],
capture_output=True, text=True, timeout=60,
cwd=DEPLOY_DIR)
return p.stdout
except Exception as e:
return str(e)
# ================================================================
# T6: Git 日志
# ================================================================
@server.tool()
async def git_log(count: int = 10):
"""查看 project Git 提交历史"""
try:
p = subprocess.run(['git', 'log', f'-{count}', '--oneline'],
capture_output=True, text=True, timeout=5,
cwd=DEPLOY_DIR)
return p.stdout
except Exception as e:
return str(e)
# ================================================================
# T7: 部署同步
# ================================================================
@server.tool()
async def deploy():
"""同步最新代码到运行目录并重启服务(需通过 pre-deploy 门控检查)"""
# ── Pull latest code first ──
try:
pull = subprocess.run(
f"cd {DEPLOY_DIR} && git fetch --all 2>&1 && git reset --hard origin/main 2>&1",
shell=True, capture_output=True, text=True, timeout=30,
)
if pull.returncode != 0:
return f"🚫 Git pull failed:\n\n{pull.stdout}\n{pull.stderr}"
except Exception as e:
return f"🚫 Git pull error: {e}"
# ── Gate Check (after pull so files are up to date) ──
gate_script = os.path.join(DEPLOY_DIR, "deploy", "pre_deploy_check.sh")
if os.path.exists(gate_script):
try:
g = subprocess.run(
['bash', gate_script],
capture_output=True, text=True, timeout=30,
env={**os.environ, "NEXUS_DEPLOY_DIR": DEPLOY_DIR},
)
if g.returncode != 0:
return f"🚫 DEPLOY BLOCKED — Pre-deploy gate check failed:\n\n{g.stdout}\n\nFix the blocked gates before deploying."
except Exception as e:
return f"🚫 Gate check error: {e}"
else:
# Gate script not on server yet — warn but allow (bootstrap scenario)
pass
# ── Restart service ──
try:
p = subprocess.run("supervisorctl restart nexus 2>&1", shell=True, capture_output=True, text=True, timeout=30)
return f"Pull:\n{pull.stdout}\n---\nRestart:\n{p.stdout or p.stderr or 'OK'}"
except Exception as e:
return str(e)
# ================================================================
# T8: 配置查看
# ================================================================
@server.tool()
async def config_view():
"""查看当前非敏感配置(隐藏密码/密钥)"""
try:
with open(os.path.join(DEPLOY_DIR, '.env')) as f:
lines = []
for line in f:
line = line.strip()
if not line or line.startswith('#'):
lines.append(line)
continue
for sensitive in ('DATABASE_URL', 'SECRET_KEY', 'API_KEY', 'ADMIN_PASSWORD',
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'ENCRYPTION_KEY'):
if line.startswith(f'NEXUS_{sensitive}='):
key = line.split('=', 1)[0]
lines.append(f'{key}=***REDACTED***')
break
else:
lines.append(line)
return '\n'.join(lines)
except Exception as e:
return str(e)
# ================================================================
# T9: 门控日志
# ================================================================
@server.tool()
async def gate_log(lines: int = 20):
"""查看门控执行日志。lines: 显示最近N条记录"""
try:
log_path = os.path.join(DEPLOY_DIR, "deploy", "gate_log.jsonl")
if not os.path.exists(log_path):
return "No gate log found — no gate checks have been recorded yet"
result = []
with open(log_path, encoding="utf-8") as f:
all_lines = f.readlines()
for line in all_lines[-lines:]:
d = json.loads(line)
icon = {"PASS": "", "BLOCK": "🚫", "SKIP": "⏭️", "WARN": "⚠️"}.get(d["result"], "?")
ts = d.get("ts", "")[:19]
gate = d.get("gate", "?")
detail = d.get("detail", "")
result.append(f"{ts} {icon} {gate}: {detail}")
return "\n".join(result) if result else "No entries"
except Exception as e:
return str(e)
# ================================================================
# Main
# ================================================================
if __name__ == "__main__":
import asyncio
asyncio.run(server.run_stdio_async())
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Firefox Browser MCP — 控制 Firefox 进行 Web 测试"""
import json, time, base64, asyncio
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("firefox-mcp")
driver = None
def _get_driver():
global driver
if driver is None:
opts = Options()
opts.add_argument("--headless")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
driver = webdriver.Firefox(options=opts)
driver.set_page_load_timeout(30)
return driver
# ================================================================
# T1: 导航
# ================================================================
@server.tool()
async def firefox_navigate(url: str = "https://192.168.124.10/app/login.html"):
"""Firefox 打开指定 URL,返回页面标题和 URL"""
try:
d = _get_driver()
d.get(url)
return f"标题: {d.title}\nURL: {d.current_url}"
except Exception as e:
return f"导航失败: {e}"
# ================================================================
# T2: 截图
# ================================================================
@server.tool()
async def firefox_screenshot():
"""截取当前页面截图,返回 base64 PNG"""
try:
d = _get_driver()
png = d.get_screenshot_as_base64()
return f"截图大小: {len(png)} bytes (base64). 可用工具查看。\n前100字符: {png[:100]}..."
except Exception as e:
return f"截图失败: {e}"
# ================================================================
# T3: 获取页面文本
# ================================================================
@server.tool()
async def firefox_get_text():
"""获取当前页面可见文本内容(用于检查错误信息)"""
try:
d = _get_driver()
body = d.find_element(By.TAG_NAME, "body")
text = body.text[:3000]
return text if text else "(空页面)"
except Exception as e:
return str(e)
# ================================================================
# T4: 填充输入框并提交
# ================================================================
@server.tool()
async def firefox_fill(selector: str = "input[name='username']", value: str = "admin"):
"""在指定 CSS selector 的输入框中填入值"""
try:
d = _get_driver()
el = d.find_element(By.CSS_SELECTOR, selector)
el.clear()
el.send_keys(value)
return f"已填入 {selector} = {value}"
except Exception as e:
return str(e)
# ================================================================
# T5: 点击
# ================================================================
@server.tool()
async def firefox_click(selector: str = "button[type='submit']"):
"""点击指定 CSS selector 的元素"""
try:
d = _get_driver()
el = d.find_element(By.CSS_SELECTOR, selector)
el.click()
time.sleep(2)
return f"已点击 {selector}。当前页面: {d.title} ({d.current_url})"
except Exception as e:
return str(e)
# ================================================================
# T6: 控制台日志
# ================================================================
@server.tool()
async def firefox_console():
"""获取浏览器控制台日志"""
try:
d = _get_driver()
logs = d.get_log("browser")
entries = [f"[{e['level']}] {e['message'][:200]}" for e in logs[-20:]]
return "\n".join(entries) if entries else "(无日志)"
except Exception as e:
return str(e)
# ================================================================
# Main
# ================================================================
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
+29
View File
@@ -0,0 +1,29 @@
"""MCP Bridge — launches remote MCP server via SSH and bridges stdio
Configure via environment variables:
NEXUS_SSH_KEY — path to SSH private key (default: ~/.ssh/id_rsa)
NEXUS_SSH_HOST — remote server hostname/IP (required)
NEXUS_DEPLOY_DIR — remote installation path (default: /opt/nexus)
"""
import os
import sys
import subprocess
from pathlib import Path
ssh_key = os.environ.get("NEXUS_SSH_KEY", str(Path.home() / ".ssh" / "id_rsa"))
ssh_host = os.environ.get("NEXUS_SSH_HOST", "")
deploy_dir = os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus")
if not ssh_host:
print("ERROR: NEXUS_SSH_HOST not set (e.g. root@1.2.3.4)", file=sys.stderr)
sys.exit(1)
ssh = subprocess.Popen([
"ssh",
"-o", "StrictHostKeyChecking=no",
"-i", ssh_key,
ssh_host,
"python3", f"{deploy_dir}/mcp/Nexus_server.py"
], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
sys.exit(ssh.wait())