merge: 恢复077c649内容(开发原则/K tests/spec/prd paths) + 保留flicker-free EasyTier/RDP + Linux兼容改善
- dashboard.html: 应用fI()闪烁修复到077c649版本(create-once/update-state pattern) - dashboard.py: 新增 /api/tests 端点 (import tests_api) - dashboard.py: /api/git 改用 git log 命令优先, 降级到 reflog - dashboard.py: /api/monitor + /api/expected 增加 Linux 支持(systemd timer/crontab) - dashboard.py: /api/spec + /api/prd 增加 246 venv 路径候选 - dashboard.py: /api/metagrowth git路径探测改善 - 恢复: meta_growth.py, tests_api.py, checklist_audit.py, service_registry.py - 恢复: sync-venv.sh, post-deploy-check.sh, meta-growth-design.md
This commit is contained in:
+202
-68
@@ -9,7 +9,7 @@ Flask app on :5803. Monitors agents across platforms via:
|
||||
|
||||
Auto-recovery: restarts local Windows agents after 3 consecutive offline checks.
|
||||
"""
|
||||
import os, sys, re, json, time, subprocess, logging, urllib.request, sqlite3
|
||||
import os, sys, re, json, time, subprocess, logging, urllib.request, sqlite3, shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
@@ -35,6 +35,16 @@ if _PROJECT_OVERRIDE:
|
||||
LOGS_DIR = _GATEWAY_DIR / "logs"
|
||||
TEMP_DIR = _GATEWAY_DIR / "temp"
|
||||
|
||||
# Auto-recovery script name map (used by /api/agents)
|
||||
SCRIPT_NAMES = {
|
||||
"xmpp_bot": "xmpp_bot.py",
|
||||
"wechat_bridge": "wechat_agent.py",
|
||||
"api_proxy": "api_proxy.py",
|
||||
"health_check": "health_check_xxm.py",
|
||||
"mohe_watcher": "mohe_watcher.py",
|
||||
"watchdog": "xmpp_watchdog.py",
|
||||
}
|
||||
|
||||
sys.path.insert(0, str(_GATEWAY_DIR / "scripts"))
|
||||
from proc_guard import guard
|
||||
|
||||
@@ -564,34 +574,77 @@ def api_kanban():
|
||||
def api_git():
|
||||
"""最近 git 提交历史 + 分支状态(从 .git 直接读取,不依赖 git 命令)"""
|
||||
try:
|
||||
git_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
# 先用 _PROJECT_DIR(已处理 AGENTSMEETING_ROOT 覆盖),不行再探测常见路径
|
||||
candidates = [str(_PROJECT_DIR)]
|
||||
if _PROJECT_OVERRIDE:
|
||||
candidates.append(str(_PROJECT_DIR))
|
||||
# 246 生产环境常见路径
|
||||
if sys.platform != "win32":
|
||||
candidates.extend([
|
||||
"/home/hmo/projects/AgentsMeeting",
|
||||
os.path.expanduser("~/projects/AgentsMeeting"),
|
||||
])
|
||||
git_dir = None
|
||||
for c in candidates:
|
||||
if os.path.exists(os.path.join(c, ".git")):
|
||||
git_dir = c
|
||||
break
|
||||
if not git_dir:
|
||||
return jsonify({"ok": True, "log": [], "dirty": False, "note": "未找到.git目录,请设置AGENTSMEETING_ROOT"})
|
||||
dot_git = os.path.join(git_dir, ".git")
|
||||
if not os.path.exists(dot_git):
|
||||
alt = os.environ.get("AGENTSMEETING_ROOT", "")
|
||||
if alt and os.path.exists(os.path.join(alt, ".git")):
|
||||
git_dir = alt
|
||||
dot_git = os.path.join(alt, ".git")
|
||||
else:
|
||||
return jsonify({"ok": True, "log": [], "dirty": False, "note": "no .git"})
|
||||
# 从 .git/logs/HEAD 读取最近提交
|
||||
reflog = os.path.join(dot_git, "logs", "HEAD")
|
||||
log_lines = []
|
||||
if os.path.exists(reflog):
|
||||
with open(reflog, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
# 取最后 10 行非空行,格式: "hash hash user ... msg"
|
||||
for line in reversed(lines):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
msg = parts[-1] if len(parts) > 1 else line
|
||||
# 取前 7 位 hash
|
||||
hash_part = line.split()[1][:7] if len(line.split()) > 1 else "?"
|
||||
log_lines.append(f"{hash_part} {msg}")
|
||||
if len(log_lines) >= 10:
|
||||
break
|
||||
# 检查是否有未提交的改动:比较 .git/index 的 mtime 与 HEAD
|
||||
# 优先用 git log 命令(真实提交信息),降级到解析 reflog
|
||||
tried_git = False
|
||||
# Linux nohup 环境 PATH 可能不含 /usr/bin,必须用绝对路径
|
||||
GIT_CANDIDATES = [
|
||||
os.environ.get("GIT_CMD", ""),
|
||||
shutil.which("git") or "",
|
||||
"/usr/bin/git", "/usr/local/bin/git",
|
||||
]
|
||||
git_bin = ""
|
||||
for c in GIT_CANDIDATES:
|
||||
if c and os.path.exists(c):
|
||||
git_bin = c
|
||||
break
|
||||
if git_bin:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[git_bin, "log", "--format=%h|%ad|%s", "--date=format:%m-%d %H:%M", "--no-decorate"],
|
||||
cwd=git_dir, capture_output=True, text=True, timeout=10)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
tried_git = True
|
||||
for line in r.stdout.strip().split("\n"):
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split("|", 2)
|
||||
if len(parts) == 3:
|
||||
log_lines.append(f"{parts[0]} {parts[1]} {parts[2]}")
|
||||
else:
|
||||
log_lines.append(line)
|
||||
except:
|
||||
pass
|
||||
if not tried_git:
|
||||
reflog = os.path.join(dot_git, "logs", "HEAD")
|
||||
if os.path.exists(reflog):
|
||||
with open(reflog, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
for line in reversed(lines):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
msg = parts[-1] if len(parts) > 1 else line
|
||||
hash_part = line.split()[1][:7] if len(line.split()) > 1 else "?"
|
||||
# 从 reflog 提取时间戳: 第3+4字段 = unix时间 + 时区
|
||||
ts = ""
|
||||
try:
|
||||
cols = line.split()
|
||||
if len(cols) >= 5:
|
||||
ts = datetime.fromtimestamp(int(cols[3])).strftime("%m-%d %H:%M") + " "
|
||||
except:
|
||||
pass
|
||||
log_lines.append(f"{ts}{hash_part} {msg}")
|
||||
# 检查是否有未提交的改动
|
||||
dirty = False
|
||||
head_file = os.path.join(dot_git, "HEAD")
|
||||
index_file = os.path.join(dot_git, "index")
|
||||
@@ -600,7 +653,16 @@ def api_git():
|
||||
dirty = os.path.getmtime(index_file) > os.path.getmtime(head_file)
|
||||
except:
|
||||
pass
|
||||
return jsonify({"ok": True, "log": log_lines, "dirty": dirty, "branch": "master", "source": "reflog"})
|
||||
# 分支名
|
||||
branch = "master"
|
||||
try:
|
||||
with open(head_file, "r") as f:
|
||||
ref = f.read().strip()
|
||||
if ref.startswith("ref: refs/heads/"):
|
||||
branch = ref[16:]
|
||||
except:
|
||||
pass
|
||||
return jsonify({"ok": True, "log": log_lines, "dirty": dirty, "branch": branch, "source": git_dir, "method": ("git_log" if tried_git else "reflog")})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e), "log": [], "dirty": False})
|
||||
|
||||
@@ -659,7 +721,7 @@ def _health_status(url, timeout=3):
|
||||
@app.route("/api/monitor")
|
||||
def api_monitor():
|
||||
"""Tier1/Tier2 最近检查结果 + 定时任务状态"""
|
||||
result = {"tier1": None, "tier2": None, "tasks": []}
|
||||
result = {"ok": True, "tier1": None, "tier2": None, "tasks": [], "platform": sys.platform}
|
||||
# Tier1 最新报告
|
||||
t1_file = TEMP_DIR / "last_health_check.json"
|
||||
if t1_file.exists():
|
||||
@@ -668,6 +730,8 @@ def api_monitor():
|
||||
result["tier1"] = json.load(f)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
result["tier1"] = {"status": "no_data", "note": f"{t1_file} 不存在", "summary": {"ok": 0, "total": 0}}
|
||||
# Tier2 最新报告
|
||||
t2_file = TEMP_DIR / "last_daily_health.json"
|
||||
if t2_file.exists():
|
||||
@@ -676,18 +740,37 @@ def api_monitor():
|
||||
result["tier2"] = json.load(f)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
result["tier2"] = {"status": "no_data", "note": f"{t2_file} 不存在", "summary": {"ok": 0, "total": 0}}
|
||||
# 定时任务状态
|
||||
expected_tasks = ["agents-health-check", "agents-daily-health", "agents-todo-executor"]
|
||||
for name in expected_tasks:
|
||||
try:
|
||||
r = subprocess.run(["schtasks", "/Query", "/TN", name, "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
# 兼容中文"就绪"和英文"Ready"
|
||||
status = "ok" if ("Ready" in r.stdout or "\u5c31\u7eea" in r.stdout) else ("missing" if name not in r.stdout else "other")
|
||||
except:
|
||||
status = "error"
|
||||
result["tasks"].append({"name": name, "status": status})
|
||||
if sys.platform == "win32":
|
||||
for name in expected_tasks:
|
||||
try:
|
||||
r = subprocess.run(["schtasks", "/Query", "/TN", name, "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
status = "ok" if ("Ready" in r.stdout or "\u5c31\u7eea" in r.stdout) else ("missing" if name not in r.stdout else "other")
|
||||
except:
|
||||
status = "error"
|
||||
result["tasks"].append({"name": name, "status": status})
|
||||
else:
|
||||
# Linux: 检查 systemd timer(如已部署)或 crontab
|
||||
for name in expected_tasks:
|
||||
try:
|
||||
r = subprocess.run(["systemctl", "list-timers", "--all", "--no-pager"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if name in r.stdout:
|
||||
result["tasks"].append({"name": name, "status": "timer_ok"})
|
||||
else:
|
||||
# 查 crontab
|
||||
r2 = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=5)
|
||||
if name in r2.stdout:
|
||||
result["tasks"].append({"name": name, "status": "cron_ok"})
|
||||
else:
|
||||
result["tasks"].append({"name": name, "status": "not_deployed"})
|
||||
except:
|
||||
result["tasks"].append({"name": name, "status": "not_deployed", "note": "无systemd/cron"})
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@@ -714,16 +797,21 @@ def api_metagrowth():
|
||||
continue
|
||||
if entry.get("status") == "completed":
|
||||
completed_fixes += 1
|
||||
# 统计 git 提交次数(从 reflog 行数计算)
|
||||
git_dir2 = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
# 统计 git 提交次数(从 reflog 行数计算,跟 api_git 同样路径探测逻辑)
|
||||
commit_count = 0
|
||||
reflog = os.path.join(git_dir2, ".git", "logs", "HEAD")
|
||||
if os.path.exists(reflog):
|
||||
try:
|
||||
with open(reflog, "r", encoding="utf-8", errors="replace") as f:
|
||||
commit_count = sum(1 for line in f if line.strip())
|
||||
except:
|
||||
pass
|
||||
git_candidates = [str(_PROJECT_DIR)]
|
||||
if sys.platform != "win32":
|
||||
git_candidates.extend(["/home/hmo/projects/AgentsMeeting",
|
||||
os.path.expanduser("~/projects/AgentsMeeting")])
|
||||
for c in git_candidates:
|
||||
rfl = os.path.join(c, ".git", "logs", "HEAD")
|
||||
if os.path.exists(rfl):
|
||||
try:
|
||||
with open(rfl, "r", encoding="utf-8", errors="replace") as f:
|
||||
commit_count = sum(1 for line in f if line.strip())
|
||||
except:
|
||||
pass
|
||||
break
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"status": "planned", # Phase 3
|
||||
@@ -788,28 +876,55 @@ def api_expected():
|
||||
if port:
|
||||
actual[name] = "running" if port_open(port) else "stopped"
|
||||
elif name == "watchdog":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["tasklist", "/FI", "IMAGENAME eq python.exe", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
actual[name] = "running" if "python" in r.stdout else "stopped"
|
||||
except:
|
||||
actual[name] = "unknown"
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["tasklist", "/FI", "IMAGENAME eq python.exe", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
actual[name] = "running" if "python" in r.stdout else "stopped"
|
||||
except:
|
||||
actual[name] = "unknown"
|
||||
else:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["pgrep", "-f", "xmpp_watchdog|health_check"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
actual[name] = "running" if r.stdout.strip() else "stopped"
|
||||
except:
|
||||
# pgrep not available
|
||||
r = subprocess.run(
|
||||
["ps", "aux"], capture_output=True, text=True, timeout=5)
|
||||
has = "watchdog" in r.stdout or "health_check" in r.stdout
|
||||
actual[name] = "running" if has else "stopped"
|
||||
elif "scheduled" in e["expected"]:
|
||||
task_name = name
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["schtasks", "/Query", "/TN", task_name, "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
# 中文"就绪"、英文"Ready"
|
||||
ready = "Ready" in r.stdout or "\u5c31\u7eea" in r.stdout
|
||||
actual[name] = "scheduled" if ready else "missing"
|
||||
except:
|
||||
actual[name] = "error"
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["schtasks", "/Query", "/TN", task_name, "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
# 中文"就绪"、英文"Ready"
|
||||
ready = "Ready" in r.stdout or "\u5c31\u7eea" in r.stdout
|
||||
actual[name] = "scheduled" if ready else "missing"
|
||||
except:
|
||||
actual[name] = "error"
|
||||
else:
|
||||
# Linux: 检查 systemd timer 或 crontab
|
||||
try:
|
||||
r = subprocess.run(["systemctl", "list-timers", "--all", "--no-pager"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if task_name in r.stdout:
|
||||
actual[name] = "timer_ok"
|
||||
else:
|
||||
r2 = subprocess.run(["crontab", "-l"], capture_output=True,
|
||||
text=True, timeout=5)
|
||||
actual[name] = "cron_ok" if task_name in r2.stdout else "not_deployed"
|
||||
except:
|
||||
actual[name] = "not_deployed"
|
||||
return jsonify({"expected": expected, "actual": actual})
|
||||
|
||||
|
||||
@@ -928,6 +1043,9 @@ def api_spec():
|
||||
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"),
|
||||
# 246 venv: ~/ (5 dirname ups from venv/dashboard.py)
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))), ".memory", "dev-spec.md"),
|
||||
os.path.expanduser("~/.memory/dev-spec.md"),
|
||||
]
|
||||
spec_path = None
|
||||
for c in candidates:
|
||||
@@ -951,6 +1069,9 @@ def api_prd():
|
||||
candidates = [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "docs", "PRD.md"),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "docs", "PRD.md"),
|
||||
# venv deployment: ~/projects/AgentsMeeting/docs/PRD.md
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))), "projects", "AgentsMeeting", "docs", "PRD.md"),
|
||||
os.path.expanduser("~/projects/AgentsMeeting/docs/PRD.md"),
|
||||
]
|
||||
prd_path = None
|
||||
for c in candidates:
|
||||
@@ -968,6 +1089,19 @@ def api_prd():
|
||||
return jsonify({"ok": False, "error": str(e), "content": ""})
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# K — 自动测试接口
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/tests")
|
||||
def api_tests():
|
||||
"""运行自动测试并返回结果"""
|
||||
try:
|
||||
from tests_api import run_tests
|
||||
return jsonify(run_tests())
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)})
|
||||
|
||||
|
||||
@app.route("/api/platform")
|
||||
@app.route("/api/health")
|
||||
def api_health():
|
||||
|
||||
Reference in New Issue
Block a user