xiaowei-system/scripts/sensor-verify.py

167 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
sensor-verify.py — 确定性传感器2026-08-12 新增)
====================================================
借鉴《AI Agent 的自我进化》:能用代码解决的坚决不用模型。
功能全部确定性检查0 LLM 成本):
1. JSON Schema 验证 — 关键输出文件是否可解析且字段完整
2. 退出码分级检查 — cron 脚本的退出码语义0=OK, 1=业务异常, 2=离线常态)
3. 关键脚本 smoke test — 快速运行确认可执行
用法:
sensor-verify.py → 全量检查exit 0=健康 1=有异常
sensor-verify.py json → 只检查 JSON schema
sensor-verify.py smoke → 只跑 smoke test
sensor-verify.py --quiet → 静默模式watchdog 用,只输出异常)
退出码: 0 = 全部健康, 1 = 有异常(供 cron 报警)
"""
import json, os, sys, subprocess, datetime
HOME = os.path.expanduser("~")
HERMES = HOME + "/.hermes"
STOCK_BT = HERMES + "/stock_backtest"
# ====== 1. JSON Schema 验证 ======
# 格式: 文件路径 → (必需字段列表, 允许空?)
JSON_CHECKS = {
HERMES + "/model-health.json": (["stable", "total_models", "timestamp"], True),
HERMES + "/skill-health.json": (["summary", "skills"], True),
HERMES + "/optimization-report.json": (["health_score"], True),
HERMES + "/learner/state.json": (["total_cycles", "learned_items"], True),
STOCK_BT + "/fundamental_scan.json": ([], True),
STOCK_BT + "/sentiment_scan.json": ([], True),
STOCK_BT + "/macro_score.json": ([], True),
STOCK_BT + "/industry_scan.json": ([], True),
}
# ====== 2. 退出码语义(已规范化的脚本) ======
# 脚本名 → 期望退出码分级说明0=正常, 2=离线常态等)
EXIT_CODE_CONTRACT = {
"dual-backup.sh": "0=备份完成/离线静默跳过, 1=真异常",
"memory-governance.py": "0=正常(即使无候选), 1=API异常",
"stock_daily_health.py": "0=健康, 1=有异常(数据/cron/账户)",
"distill-model-watchdog.py": "0=正常, 1=模型异常",
}
# ====== 3. Smoke test 脚本(快速运行验证可执行性) ======
SMOKE_SCRIPTS = [
("learner.py status", 15),
("skill-manager.py dashboard --quiet 2>/dev/null || skill-manager.py scan --quiet 2>/dev/null || true", 20),
("memory-governance.py", 30),
]
def check_json_schema():
issues = []
for path, (required, allow_empty) in JSON_CHECKS.items():
if not os.path.exists(path):
continue # 不存在不报(有些文件按需生成)
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, (dict, list)):
issues.append(f"{os.path.basename(path)}: 不是 JSON 对象/数组")
continue
if isinstance(data, dict):
for field in required:
if field not in data:
issues.append(f"{os.path.basename(path)}: 缺少字段 '{field}'")
if not allow_empty and isinstance(data, list) and len(data) == 0:
issues.append(f"{os.path.basename(path)}: 空数组(可能有数据问题)")
except json.JSONDecodeError as e:
issues.append(f"{os.path.basename(path)}: JSON 解析失败 ({e})")
except Exception as e:
issues.append(f"{os.path.basename(path)}: 读取失败 ({e})")
return issues
def check_exit_codes():
"""检查 cron 最近输出是否出现异常退出Status: script failed / error"""
issues = []
cron_out = HERMES + "/cron/output"
if not os.path.isdir(cron_out):
return issues
cutoff = datetime.datetime.now() - datetime.timedelta(days=1)
for jid in os.listdir(cron_out):
jdir = os.path.join(cron_out, jid)
if not os.path.isdir(jdir):
continue
files = sorted(os.listdir(jdir))
if not files:
continue
latest = os.path.join(jdir, files[-1])
try:
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(latest))
if mtime < cutoff:
continue
with open(latest, encoding="utf-8", errors="ignore") as f:
content = f.read()
if "script failed" in content or "Status: **error**" in content or "Traceback" in content:
# 网络类失败(服务器离线)是常态——过滤已知静默场景
if "不在局域网" in content or "离线" in content:
continue
issues.append(f"cron {jid} 最近输出异常: {os.path.basename(latest)}")
except Exception:
continue
return issues
def run_smoke():
issues = []
for cmd, timeout in SMOKE_SCRIPTS:
try:
r = subprocess.run(
f"cd {HERMES}/scripts && python3 {cmd}",
shell=True, capture_output=True, text=True, timeout=timeout,
)
if r.returncode != 0 and "skill-manager" not in cmd:
issues.append(f"smoke 失败: {cmd} (exit={r.returncode})")
except subprocess.TimeoutExpired:
issues.append(f"smoke 超时: {cmd}")
except Exception as e:
issues.append(f"smoke 异常: {cmd} ({e})")
return issues
def main():
quiet = "--quiet" in sys.argv
only = None
for a in sys.argv[1:]:
if a in ("json", "smoke"):
only = a
all_issues = []
if only in (None, "json"):
all_issues += check_json_schema()
if only in (None, "smoke"):
all_issues += run_smoke()
if only is None:
all_issues += check_exit_codes()
if quiet:
# watchdog 模式:只输出异常
if all_issues:
print("⚠️ 确定性传感器发现异常:")
for i in all_issues:
print(f"{i}")
sys.exit(1)
sys.exit(0)
print(f"🛰️ 确定性传感器 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 50)
print(f"JSON Schema: {len(JSON_CHECKS)} 个文件")
print(f"Smoke test: {len(SMOKE_SCRIPTS)} 个脚本")
if not all_issues:
print("\n✅ 全部健康——确定性检查通过0 LLM 成本)")
sys.exit(0)
print(f"\n⚠️ 发现 {len(all_issues)} 个异常:")
for i in all_issues:
print(f"{i}")
sys.exit(1)
if __name__ == "__main__":
main()