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:
hmo
2026-07-15 10:10:25 +08:00
parent ed165db8c9
commit b11b88887c
8 changed files with 1191 additions and 117 deletions
+202 -68
View File
@@ -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():