Files
MoFin/deploy/profile-scripts/agent_spiral_watchdog.py
T
hmo 1e71a2d852 feat(alerts): alert_helper 统一告警网关——信噪比控制
两级通道:
- ACTION(买入信号/重点推荐/部署验证失败): 直通不限速, 🚨醒目前缀
- INFO(部署/卫生/修复/螺旋报备): 同类30min限1条 + ≤8行截断 +
  24h内容去重(同一问题不重复轰炸) + 压制计数透明披露

6个调用点全部迁移: deploy_guard/hygiene/spiral/self_repair(INFO)
+ batch_reassess/per_stock_reassess 买入信号(ACTION)
解决: 真正有意义的信息(重点推荐操作)不被纯通知淹没
2026-07-21 02:43:57 +08:00

124 lines
4.1 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
"""agent_spiral_watchdog.py — agent 会话螺旋检测(每 10 分钟 cron)
盲区①的系统性补丁(2026-07-21 603288 事件:一次重评请求在 hermes agent
运行时里螺旋 35 分钟/37 轮工具/153k token,无任何组件察觉)。
检测:各 profile state.db 中仍开放的 api_server session
启动超 15 分钟且 message_count > 80(正常调用 <20 条)→ 螺旋嫌疑。
v1 动作:XMPP 告警 + 日志(不自动 kill——kill 需要 hermes 会话取消 API
待确认安全机制后升级 v2)。同一 session 只告警一次(state 文件去重)。
"""
import json, os, sqlite3, glob
from datetime import datetime
STATE_FILE = "/home/hmo/MoFin/gateway/logs/spiral_watchdog_state.json"
LOG = "/home/hmo/MoFin/gateway/logs/spiral_watchdog.log"
MIN_AGE_SEC = 15 * 60
MIN_MESSAGES = 80
def log(msg):
line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
print(line, flush=True)
try:
os.makedirs(os.path.dirname(LOG), exist_ok=True)
with open(LOG, "a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception:
pass
def xmpp(msg):
try:
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from alert_helper import notify, INFO
notify("螺旋监控", msg, INFO)
except Exception as e:
log(f"XMPP发送失败: {e}")
def load_state():
try:
with open(STATE_FILE, encoding="utf-8") as f:
return json.load(f)
except Exception:
return {"alerted": []}
def save_state(st):
try:
st["alerted"] = st.get("alerted", [])[-50:]
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(st, f)
except Exception:
pass
def _to_ms(ts):
"""started_at 单位自适应:>1e12 视为毫秒,否则视为秒。"""
if not ts:
return 0
return ts if ts > 1e12 else ts * 1000
def main():
st = load_state()
alerted = set(st.get("alerted", []))
now_ms = datetime.now().timestamp() * 1000
found = 0
for db_path in glob.glob("/home/hmo/.hermes/profiles/*/state.db"):
profile = db_path.split("/")[-2]
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=10)
conn.row_factory = sqlite3.Row
tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if "sessions" not in tables:
conn.close()
continue
rows = conn.execute("""
SELECT id, started_at, message_count, input_tokens, tool_call_count
FROM sessions
WHERE source = 'api_server'
AND (ended_at IS NULL OR ended_at = 0)
ORDER BY started_at DESC LIMIT 30
""").fetchall()
conn.close()
except Exception as e:
log(f"[{profile}] state.db 读取失败: {str(e)[:80]}")
continue
for r in rows:
sa = _to_ms(r["started_at"])
age_sec = (now_ms - sa) / 1000
if age_sec < MIN_AGE_SEC:
continue
if (r["message_count"] or 0) < MIN_MESSAGES:
continue
sid = r["id"]
if sid in alerted:
continue
found += 1
alerted.add(sid)
msg = (f"agent 螺旋嫌疑\n"
f"profile: {profile}\n"
f"session: {sid}\n"
f"已运行: {age_sec/60:.0f} 分钟\n"
f"消息数: {r['message_count']} | 工具调用: {r['tool_call_count']} | "
f"输入token: {r['input_tokens']}\n"
f"特征类似 603288 事件。正常长任务可忽略;否则需人工检查。")
log(f"SPIRAL: [{profile}] {sid} age={age_sec/60:.0f}min msgs={r['message_count']} tools={r['tool_call_count']}")
xmpp(msg)
st["alerted"] = sorted(alerted)
save_state(st)
log(f"── 结束: 新告警 {found} ──")
return 0
if __name__ == "__main__":
raise SystemExit(main())