From b11b88887cd7d1d662a0b3e5af61128fd8229922 Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 15 Jul 2026 10:10:25 +0800 Subject: [PATCH] =?UTF-8?q?merge:=20=E6=81=A2=E5=A4=8D077c649=E5=86=85?= =?UTF-8?q?=E5=AE=B9(=E5=BC=80=E5=8F=91=E5=8E=9F=E5=88=99/K=20tests/spec/p?= =?UTF-8?q?rd=20paths)=20+=20=E4=BF=9D=E7=95=99flicker-free=20EasyTier/RDP?= =?UTF-8?q?=20+=20Linux=E5=85=BC=E5=AE=B9=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- deploy/linux/post-deploy-check.sh | 87 +++++ deploy/linux/sync-venv.sh | 75 +++++ docs/meta-growth-design.md | 69 ++++ gateway/scripts/checklist_audit.py | 29 +- gateway/scripts/dashboard.py | 270 ++++++++++++---- gateway/scripts/meta_growth.py | 388 +++++++++++++++++++++++ gateway/scripts/templates/dashboard.html | 118 ++++--- gateway/scripts/tests_api.py | 272 ++++++++++++++++ 8 files changed, 1191 insertions(+), 117 deletions(-) create mode 100644 deploy/linux/post-deploy-check.sh create mode 100644 deploy/linux/sync-venv.sh create mode 100644 docs/meta-growth-design.md create mode 100644 gateway/scripts/meta_growth.py create mode 100644 gateway/scripts/tests_api.py diff --git a/deploy/linux/post-deploy-check.sh b/deploy/linux/post-deploy-check.sh new file mode 100644 index 0000000..3b2a2ea --- /dev/null +++ b/deploy/linux/post-deploy-check.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# post-deploy-check.sh — 部署后验证:K 测试无意外失败才算部署完成 +# +# 用法: bash deploy/linux/post-deploy-check.sh +# 依赖: sync-venv.sh 先执行完毕, dashboard 已在运行 +# +# 退出码: +# 0 = 验证通过(所有失败项均为 expected) +# 1 = 发现意外失败(需回滚或手动处理) + +set -e + +DASHBOARD_URL="http://127.0.0.1:5803" +TIMEOUT=10 + +echo "=== post-deploy-check: $(date) ===" + +# 1. 等待 dashboard 就绪 +echo "--- 等待 dashboard 就绪 ---" +for i in $(seq 1 6); do + if curl -s -o /dev/null -w "" --max-time 3 "$DASHBOARD_URL/" 2>/dev/null; then + echo "dashboard 已就绪 (尝试 $i)" + break + fi + if [ "$i" -eq 6 ]; then + echo "ERROR: dashboard 未能启动" + exit 1 + fi + sleep 2 +done + +# 2. 获取 K 测试结果 +echo "--- 获取 K 测试结果 ---" +TESTS_JSON=$(curl -s --max-time "$TIMEOUT" "$DASHBOARD_URL/api/tests" 2>/dev/null) + +if [ -z "$TESTS_JSON" ]; then + echo "ERROR: /api/tests 无响应" + exit 1 +fi + +# 3. 解析并验证 +# 使用 python3 解析 JSON,提取未预期的失败 +REPORT=$(python3 -c " +import json, sys + +data = json.loads('$TESTS_JSON') +if not isinstance(data, dict): + # 可能是列表格式 + tests = data if isinstance(data, list) else [] + summary = {} +else: + tests = data.get('tests', []) + summary = data.get('summary', {}) + +total = len(tests) +passed = sum(1 for t in tests if t.get('ok')) +failed = [t for t in tests if not t.get('ok')] +expected = [t for t in failed if t.get('expected')] +unexpected = [t for t in failed if not t.get('expected')] + +print(f'TOTAL: {total}') +print(f'PASS: {passed}') +print(f'FAIL: {len(failed)} (expected: {len(expected)}, UNEXPECTED: {len(unexpected)})') +print('---') +for t in unexpected: + print(f'UNEXPECTED_FAIL: {t.get(\"name\",\"?\")} | {t.get(\"detail\",\"\")}') +for t in expected: + print(f'EXPECTED_FAIL: {t.get(\"name\",\"?\")} | {t.get(\"detail\",\"\")}') + +sys.exit(1 if unexpected else 0) +") + +echo "$REPORT" + +# 提取退出码 +EXIT_CODE=$? +if [ "$EXIT_CODE" -ne 0 ]; then + echo "" + echo "!!! 部署后验证失败:发现意外失败的测试项 !!!" + echo "运行以下命令查看详情:" + echo " curl -s $DASHBOARD_URL/api/tests | python3 -m json.tool" + exit 1 +fi + +echo "" +echo "=== post-deploy-check: 验证通过 ===" +exit 0 diff --git a/deploy/linux/sync-venv.sh b/deploy/linux/sync-venv.sh new file mode 100644 index 0000000..ebf8f4e --- /dev/null +++ b/deploy/linux/sync-venv.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# sync-venv.sh — 将 git 仓库的最新代码同步到 agentsmeeting-venv 并重启 +# +# 用法: bash deploy/linux/sync-venv.sh +# 位置: 在 246 上运行 (hmo@192.168.1.246) +# +# 原理: git 仓库 ~/projects/AgentsMeeting/ 是开发主阵地, +# 但 production dashboard 跑在 /home/hmo/agentsmeeting-venv/ 下。 +# 这个脚本把仓库中的关键文件同步到 venv 并重启服务。 + +set -e + +REPO_DIR="$HOME/projects/AgentsMeeting" +VENV_DIR="/home/hmo/agentsmeeting-venv" +DASH_LOG="/tmp/dash.log" + +echo "=== sync-venv: $(date) ===" +echo "仓库: $REPO_DIR" +echo "目标: $VENV_DIR" + +# 1. 确保仓库是最新 +cd "$REPO_DIR" +git pull --rebase origin master 2>&1 | tail -2 + +# 2. 同步关键文件 +echo "--- 同步文件 ---" +cp -v "$REPO_DIR/gateway/scripts/dashboard.py" "$VENV_DIR/dashboard.py" +cp -v "$REPO_DIR/gateway/scripts/service_registry.py" "$VENV_DIR/service_registry.py" 2>/dev/null || true +cp -v "$REPO_DIR/gateway/scripts/proc_guard.py" "$VENV_DIR/proc_guard.py" 2>/dev/null || true +cp -v "$REPO_DIR/gateway/scripts/tests_api.py" "$VENV_DIR/tests_api.py" 2>/dev/null || true +cp -v "$REPO_DIR/gateway/scripts/checklist_audit.py" "$VENV_DIR/checklist_audit.py" 2>/dev/null || true +cp -v "$REPO_DIR/gateway/scripts/templates/dashboard.html" "$VENV_DIR/templates/dashboard.html" + +# 3. 重启 dashboard +echo "--- 重启 dashboard ---" +# 使用 fuser -k :5803 杀掉占端口的老进程(pkill 可能因进程命令行不含路径而漏杀) +if fuser -k 5803/tcp 2>/dev/null; then + echo "杀掉旧 dashboard 进程 (fuser -k :5803)" + sleep 2 +fi + +# 自动检测 repo 根目录并设为 AGENTSMEETING_ROOT(A-F 端点依赖此路径找 .git 和 gateway/temp) +export AGENTSMEETING_ROOT="$REPO_DIR" +# XMPP Bridge(EasyTier/RDP 操作通过 Windows 的 xmpp_bot :5802 代理) +export XMPP_BRIDGE_URL="http://192.168.1.16:5802" +cd "$VENV_DIR" +nohup python3 dashboard.py > "$DASH_LOG" 2>&1 & +sleep 3 + +# 4. 备份(用于回滚) +BACKUP_DIR="/tmp/sync-venv-backup-$(date +%s)" +mkdir -p "$BACKUP_DIR" +cp -v "$VENV_DIR/dashboard.py" "$BACKUP_DIR/" 2>/dev/null || true +cp -v "$VENV_DIR/tests_api.py" "$BACKUP_DIR/" 2>/dev/null || true +cp -v "$VENV_DIR/templates/dashboard.html" "$BACKUP_DIR/" 2>/dev/null || true +echo "备份: $BACKUP_DIR" + +# 5. 验证 — K 测试无意外失败才算部署完成 +echo "--- 部署后验证 ---" +if ! bash "$REPO_DIR/deploy/linux/post-deploy-check.sh"; then + echo "!!! 验证失败,正在回滚 !!!" + cp -v "$BACKUP_DIR/dashboard.py" "$VENV_DIR/dashboard.py" 2>/dev/null || true + cp -v "$BACKUP_DIR/tests_api.py" "$VENV_DIR/tests_api.py" 2>/dev/null || true + cp -v "$BACKUP_DIR/templates/dashboard.html" "$VENV_DIR/templates/dashboard.html" 2>/dev/null || true + fuser -k 5803/tcp 2>/dev/null || true + sleep 2 + cd "$VENV_DIR" + nohup python3 dashboard.py > "$DASH_LOG" 2>&1 & + sleep 3 + echo "回滚完成。验证日志:" + bash "$REPO_DIR/deploy/linux/post-deploy-check.sh" 2>&1 || true + exit 1 +fi + +echo "=== sync-venv 完成: $(date) ===" diff --git a/docs/meta-growth-design.md b/docs/meta-growth-design.md new file mode 100644 index 0000000..0a382ab --- /dev/null +++ b/docs/meta-growth-design.md @@ -0,0 +1,69 @@ +# meta_growth.py — 元成长回路设计 + +## 核心逻辑 + +每次修复型 commit 后,分析变更模式 → 自动生成/扩展检测规则 → 注入 Tier1/2/3 检测管线。 + +``` +git log (merge commit diff) + → 提取 fix 类型 (根据文件路径 + 变更内容) + → 匹配规则模板 + → 生成规则 JSON + → 写入 grow_rules/ 目录 + → checklist_audit.py / health_checks 读取规则并执行 +``` + +## 修复类型分类 + +| 类型 | 触发模式 | 生成的规则 | +|------|----------|-----------| +| `port_misconfig` | 变更涉及端口号、health endpoint 路径 | 新增 port_check 规则 | +| `path_error` | 变更涉及文件路径、`os.path.join`、`Path()` | 新增 file_exists 检查 | +| `env_var` | 变更涉及 `os.environ.get`、环境变量 | 新增 env_var 检查 | +| `cmd_arg` | 变更涉及 subprocess 参数、命令行参数 | 新增 cmd_availability 检查 | +| `timeout` | 变更涉及 timeout 值、重试逻辑 | 新增 timeout_threshold 规则 | +| `import_fix` | 变更涉及 import 语句 | 新增 dependency_check 规则 | +| `permission` | 变更涉及权限、文件模式 | 新增 permission_check 规则 | + +## 规则存储格式 + +```json +{ + "rule_id": "meta-growth-001", + "source_commit": "abc1234", + "source_msg": "fix: xmpp_bot health check timeout", + "category": "timeout", + "check_type": "http_timeout", + "target": {"port": 5802, "endpoint": "/health", "timeout": 5}, + "confidence": 0.7, + "stage": 1, + "created": "2026-07-14T15:00:00", + "active": false +} +``` + +`active: false` 表示 Stage 1(安全模式)——人工确认后才激活。 + +## Stage 控制 + +| Stage | 条件 | 行为 | +|-------|------|------| +| 1 | 默认 | 规则生成但不激活,写入 `grow_rules/` + 日志 | +| 2 | 连续 3 次同类修复无退回 | 规则自动激活,注入 Tier1 检测 | +| 3 | 连续 5 次泛化无退回 | 规则自动扩展到同类组件 | + +## 输出消费方 + +- `grow_rules/*.json` → `checklist_audit.py` 读取 → K 测试中显示"元生长规则"子项 +- 激活的规则 → `agents_health_check.py` 提取 → 加入 Tier1 检测 + +## 文件结构 + +``` +gateway/scripts/ +├── meta_growth.py # 主逻辑 +├── grow_rules/ # 规则输出目录(自动创建) +│ └── growth_meta.json # 累计规则清单 +└── pattern_templates/ # 规则模板定义(手写) + └── templates.json +``` diff --git a/gateway/scripts/checklist_audit.py b/gateway/scripts/checklist_audit.py index 76c2be3..15861e5 100644 --- a/gateway/scripts/checklist_audit.py +++ b/gateway/scripts/checklist_audit.py @@ -148,7 +148,34 @@ def main(): f.write("\n".join(new_lines) + "\n") print(f" TODO 文件已清理: {len(new_lines)} 活跃条目") - # 5. Run system reality checks + # 5. Deploy status check (H0 规范) + print(f"\n🚀 部署状态 (H0):") + agents_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) + try: + r_local = subprocess.run(["git", "rev-parse", "HEAD"], cwd=agents_dir, + capture_output=True, text=True, timeout=5) + local_head = r_local.stdout.strip() if r_local.returncode == 0 else "?" + r_remote = subprocess.run(["git", "rev-parse", "origin/master"], cwd=agents_dir, + capture_output=True, text=True, timeout=5) + remote_head = r_remote.stdout.strip() if r_remote.returncode == 0 else "?" + r_unpushed = subprocess.run(["git", "log", "origin/master..HEAD", "--oneline"], cwd=agents_dir, + capture_output=True, text=True, timeout=5) + unpushed = r_unpushed.stdout.strip() + if local_head == remote_head: + print(f" ✅ 本地与 origin/master 一致: {local_head[:12]}") + else: + print(f" ⚠️ 本地与 origin/master 不同步") + print(f" local: {local_head[:12]} remote: {remote_head[:12]}") + if unpushed: + print(f" ⚠️ 有 {len(unpushed.split(chr(10)))} 个本地提交未推送:") + for line in unpushed.split(chr(10))[:3]: + print(f" {line[:60]}") + else: + print(f" ✅ 无待推送提交") + except Exception as e: + print(f" ❌ 检查失败: {str(e)[:60]}") + + # 6. Run system reality checks print(f"\n🔍 系统状态快照:") endpoints = [ ("Services", "/api/services"), diff --git a/gateway/scripts/dashboard.py b/gateway/scripts/dashboard.py index 2f8f09b..87b1254 100644 --- a/gateway/scripts/dashboard.py +++ b/gateway/scripts/dashboard.py @@ -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(): diff --git a/gateway/scripts/meta_growth.py b/gateway/scripts/meta_growth.py new file mode 100644 index 0000000..e44ddf9 --- /dev/null +++ b/gateway/scripts/meta_growth.py @@ -0,0 +1,388 @@ +""" +meta_growth.py — 元成长回路 + +分析修复型 commit 的模式,自动生成检测规则,注入监控管线。 +Phase 3 核心组件,与 Tier3 漂移检测配合使用。 + +用法: + # 分析最近修复并生成规则(手动) + python meta_growth.py --analyze + + # 查看当前规则清单 + python meta_growth.py --list + + # 激活规则(Stage 1 → 2) + python meta_growth.py --activate + +工作流: + git log (fix commits) + → diff_analyzer.py (提取变更模式) + → classifier.py (分类修复类型) + → rule_generator.py (生成检测规则) + → grow_rules/growth_meta.json (持久化) + → health_check 管线消费 +""" +import os, sys, re, json, subprocess, hashlib +from datetime import datetime, timezone + +# ─── 路径 ─────────────────────────────────────────────── +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +GROW_RULES_DIR = os.path.join(SCRIPT_DIR, "grow_rules") +TEMPLATES_PATH = os.path.join(SCRIPT_DIR, "pattern_templates", "templates.json") +GROWTH_META_PATH = os.path.join(GROW_RULES_DIR, "growth_meta.json") + +# 项目根目录(用于 git 操作) +_PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..")) +_AMS_ROOT = os.environ.get("AGENTSMEETING_ROOT", "") +if _AMS_ROOT: + _PROJECT_DIR = _AMS_ROOT + +os.makedirs(GROW_RULES_DIR, exist_ok=True) + +# ─── 修复类型分类规则 ───────────────────────────────── +FIX_PATTERNS = [ + { + "category": "port_misconfig", + "name": "端口/health 端点变更", + "patterns": [ + r'port\s*[:=]\s*\d{4,5}', + r'health.*endpoint', + r':\d{4,5}/health', + r'health_check.*url', + ], + "check_type": "port_health", + "confidence_base": 0.7, + }, + { + "category": "path_error", + "name": "文件路径变更", + "patterns": [ + r'os\.path\.(join|exists|normpath)', + r'Path\(.*\)', + r'sys\.path\.insert', + r'__file__', + r'AGENTSMEETING_ROOT', + ], + "check_type": "file_exists", + "confidence_base": 0.6, + }, + { + "category": "env_var", + "name": "环境变量变更", + "patterns": [ + r'os\.environ\.get', + r'environ\[', + r'environ\.get\(', + ], + "check_type": "env_check", + "confidence_base": 0.65, + }, + { + "category": "cmd_arg", + "name": "命令/参数变更", + "patterns": [ + r'subprocess\.run\(\[', + r'subprocess\.Popen\(\[', + r'pkill\b', + r'fuser\b', + r'pgrep\b', + ], + "check_type": "cmd_available", + "confidence_base": 0.6, + }, + { + "category": "timeout", + "name": "超时/重试变更", + "patterns": [ + r'timeout\s*[:=]\s*\d+', + r'settimeout', + r'retry\b', + r'CREATION_NO_WINDOW', + r'threaded\s*[:=]\s*True', + ], + "check_type": "timeout_threshold", + "confidence_base": 0.7, + }, + { + "category": "import_fix", + "name": "导入/依赖变更", + "patterns": [ + r'^import\s+\w+', + r'^from\s+\w+\s+import', + r'ModuleNotFoundError', + r'ImportError', + ], + "check_type": "dependency", + "confidence_base": 0.5, + }, + { + "category": "permission", + "name": "权限/安全变更", + "patterns": [ + r'chmod\b', + r'ssh\b.*key', + r'token\b', + r'repr\(', + r'escap\w+\(', + ], + "check_type": "security_scan", + "confidence_base": 0.4, + }, +] + + +def git_log(fmt="%h %ad %s", n=20, date_fmt="%m-%d %H:%M", diff_filter="", pathspec=""): + """执行 git log 返回行列表""" + cmd = ["git", "log", f"--format={fmt}", f"--date=format:{date_fmt}", f"-{n}"] + if diff_filter: + cmd.append(f"--diff-filter={diff_filter}") + if pathspec: + cmd.append("--") + cmd.append(pathspec) + try: + r = subprocess.run(cmd, cwd=_PROJECT_DIR, capture_output=True, + text=True, timeout=10, encoding="utf-8", errors="replace") + if r.returncode != 0: + return [] + return [l.strip() for l in r.stdout.split("\n") if l.strip()] + except: + return [] + + +def git_show(commit_hash): + """返回 commit 的 diff stat + 完整 diff""" + try: + r = subprocess.run( + ["git", "show", commit_hash, "--stat", "--"], + cwd=_PROJECT_DIR, capture_output=True, text=True, + timeout=10, encoding="utf-8", errors="replace") + stat = r.stdout or "" + r2 = subprocess.run( + ["git", "diff", f"{commit_hash}^..{commit_hash}", "--"], + cwd=_PROJECT_DIR, capture_output=True, text=True, + timeout=10, encoding="utf-8", errors="replace") + full_diff = r2.stdout or "" + return stat, full_diff + except: + return "", "" + + +def is_fix_commit(msg): + """判断 commit 是否为修复型提交""" + msg_lower = msg.lower() + fix_keywords = ["fix:", "bugfix", "hotfix", "correct", "修复", "修", + "resolve", "patch", "rollback", "revert"] + return any(kw in msg_lower for kw in fix_keywords) + + +def classify_fix(diff_text): + """分析 diff 文本,返回匹配的修复分类列表""" + matches = [] + for pattern_def in FIX_PATTERNS: + score = 0 + for p in pattern_def["patterns"]: + try: + if re.search(p, diff_text, re.IGNORECASE): + score += 1 + except re.error: + continue + if score > 0: + confidence = min(1.0, pattern_def["confidence_base"] + score * 0.1) + matches.append({ + "category": pattern_def["category"], + "check_type": pattern_def["check_type"], + "name": pattern_def["name"], + "confidence": round(confidence, 2), + "match_count": score, + }) + return matches + + +def generate_rule(commit_hash, commit_msg, classifications, stat_text): + """根据分类结果生成检测规则""" + rules = [] + for c in classifications: + rule_id = "meta-growth-" + hashlib.md5( + f"{commit_hash}:{c['category']}".encode()).hexdigest()[:6] + + # 从 diff stat 中提取变更的文件类型做 target 信息 + target_files = [] + if stat_text: + for line in stat_text.split("\n"): + line = line.strip() + if line.endswith(".py") or "/" in line: + target_files.append(line.split()[-1] if line.split() else line) + + rule = { + "rule_id": rule_id, + "source_commit": commit_hash, + "source_msg": commit_msg[:120], + "category": c["category"], + "check_type": c["check_type"], + "name": c["name"], + "confidence": c["confidence"], + "stage": 1, + "active": False, + "created": datetime.now(timezone.utc).isoformat(), + "target_files": target_files[:5], + "provenance": f"{commit_hash[:7]} {commit_msg[:80]}", + } + rules.append(rule) + return rules + + +def load_growth_meta(): + """加载现有规则清单""" + if os.path.exists(GROWTH_META_PATH): + try: + with open(GROWTH_META_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + pass + return {"rules": [], "stage": {"current": 1, "consecutive_fixes": 0}} + + +def save_growth_meta(data): + """保存规则清单""" + with open(GROWTH_META_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + +def merge_rules(existing, new_rules): + """合并新规则到现有清单(去重)""" + existing_ids = {r["rule_id"] for r in existing["rules"]} + for rule in new_rules: + if rule["rule_id"] not in existing_ids: + existing["rules"].append(rule) + existing_ids.add(rule["rule_id"]) + return existing + + +def stage_transition(meta): + """根据连续修复次数推进 stage""" + consecutive = meta.get("stage", {}).get("consecutive_fixes", 0) + current_stage = meta.get("stage", {}).get("current", 1) + + if current_stage == 1 and consecutive >= 3: + meta["stage"]["current"] = 2 + # 激活 stage 1 规则 + for r in meta["rules"]: + if r["stage"] == 1 and r["confidence"] >= 0.7: + r["active"] = True + print(f"[meta_growth] Stage 1 → 2 (连续 {consecutive} 次修复无退回)") + elif current_stage == 2 and consecutive >= 5: + meta["stage"]["current"] = 3 + for r in meta["rules"]: + if r["stage"] <= 2 and r["confidence"] >= 0.6: + r["active"] = True + print(f"[meta_growth] Stage 2 → 3 (连续 {consecutive} 次泛化无退回)") + + return meta + + +def analyze(commit_count=10): + """主分析流程:读取最近 fix commits → 分类 → 生成规则""" + meta = load_growth_meta() + + # 跳过已分析的 commits + analyzed_hashes = {r["source_commit"] for r in meta["rules"]} + + # 获取最近的 fix commits + commits = git_log(n=commit_count) + new_rules = [] + fix_count = 0 + + for line in commits: + parts = line.split(" ", 2) + if len(parts) < 3: + continue + h, dt, msg = parts[0], parts[1], parts[2] + if h in analyzed_hashes: + continue + if not is_fix_commit(msg): + continue + + fix_count += 1 + stat, diff = git_show(h) + classifications = classify_fix(diff) + if classifications: + rules = generate_rule(h, msg, classifications, stat) + new_rules.extend(rules) + print(f" {h} {dt} → {len(rules)} 条规则 ({', '.join(c['category'] for c in classifications)})") + + if new_rules: + meta = merge_rules(meta, new_rules) + meta["stage"]["consecutive_fixes"] = meta["stage"].get("consecutive_fixes", 0) + fix_count + meta = stage_transition(meta) + save_growth_meta(meta) + print(f"\n[meta_growth] 新增 {len(new_rules)} 条规则,累计 {len(meta['rules'])} 条") + else: + print(f"[meta_growth] 未发现新的修复模式(最近 {commit_count} 条)") + + return meta + + +def list_rules(): + """列出所有规则""" + meta = load_growth_meta() + print(f"\n元成长规则 | Stage {meta['stage']['current']} | " + f"连续修复: {meta['stage']['consecutive_fixes']} 次 | " + f"共 {len(meta['rules'])} 条规则\n") + print(f"{'ID':<22} {'类别':<16} {'置信度':<8} {'激活':<6} {'来源'}") + print("-" * 80) + for r in meta["rules"]: + active = "✅" if r.get("active") else "⏸️" + print(f"{r['rule_id']:<22} {r['category']:<16} " + f"{r['confidence']:<8} {active:<6} {r.get('provenance',''):<40}") + print() + + +def activate_rule(rule_id): + """人工激活一条规则""" + meta = load_growth_meta() + for r in meta["rules"]: + if r["rule_id"] == rule_id: + r["active"] = True + save_growth_meta(meta) + print(f"[meta_growth] 规则 {rule_id} 已激活") + return + print(f"[meta_growth] 未找到规则: {rule_id}") + + +def api_status(): + """返回状态供 dashboard E Tab 调用""" + meta = load_growth_meta() + return { + "ok": True, + "status": "running" if meta["rules"] else "initial", + "current_stage": meta["stage"]["current"], + "consecutive_fixes": meta["stage"]["consecutive_fixes"], + "total_rules": len(meta["rules"]), + "active_rules": sum(1 for r in meta["rules"] if r.get("active")), + "categories": list({r["category"] for r in meta["rules"]}), + "latest_analysis": datetime.now(timezone.utc).isoformat(), + "description": "Phase 3 元成长回路 — 分析修复模式,自动扩展检测规则", + } + + +if __name__ == "__main__": + if "--list" in sys.argv: + list_rules() + elif "--activate" in sys.argv: + idx = sys.argv.index("--activate") + if idx + 1 < len(sys.argv): + activate_rule(sys.argv[idx + 1]) + else: + print("用法: python meta_growth.py --activate ") + elif "--status" in sys.argv: + print(json.dumps(api_status(), indent=2, ensure_ascii=False)) + else: + # 默认: 分析最近 20 条 commits + n = 20 + if "--analyze" in sys.argv: + idx = sys.argv.index("--analyze") + if idx + 1 < len(sys.argv) and sys.argv[idx + 1].isdigit(): + n = int(sys.argv[idx + 1]) + meta = analyze(commit_count=n) + if "--json" in sys.argv: + print(json.dumps(meta, indent=2, ensure_ascii=False)) diff --git a/gateway/scripts/templates/dashboard.html b/gateway/scripts/templates/dashboard.html index be07f8a..fda11b1 100644 --- a/gateway/scripts/templates/dashboard.html +++ b/gateway/scripts/templates/dashboard.html @@ -1,16 +1,16 @@ - + AgentsMeeting Dashboard

AgentsMeeting

Dashboard