feat(auto_heal): multi-profile key switching + fix two error classes

Error investigation (from restored monitoring) found 2 root causes:

1. 'Blocked: script path resolves outside scripts dir' (5+ cron jobs):
   Jul 17 symlink refactor replaced real scripts with symlinks; the hermes
   cron scheduler's security check (Path.resolve + relative_to) rejects
   symlink escape. ALL no_agent script jobs blocked since Jul 17 23:12.
   FIX: converted 102 symlinks to hardlinks (same inode, resolve() stays
   inside scripts_dir, single-source still works). Permanent structural fix.

2. HTTP 429 on default profile (知识研究/梦境循环/wiki-self-growth/
   evolution-pulse/大脑任务执行): default gateway used ocg-key1 (weekly
   100%). FIX: switched default profile to ocg-key6 + added missing
   provider block. LLM verified working (3.1s).

auto_heal extended to actually cover these automatically next time:
- PROFILES registry: zhiwei (8643, system svc) + default (8642, user svc)
- current_provider/switch_key/_scan_agent_log parameterized by profile
- health() now reports llm_provider_default (agent.log scan)
- auto_heal: per-profile 429 detection -> best_key -> switch_key(profile)
- _ensure_provider_block: injects missing provider credentials from
  zhiwei config (single source of truth) into target config
This commit is contained in:
hmo
2026-07-20 01:12:38 +08:00
parent a05c118cbc
commit 57377e9dd3
8 changed files with 338 additions and 73 deletions
+45
View File
@@ -0,0 +1,45 @@
import json
d = json.load(open('/tmp/mofin_health.json'))
print("generated_at:", d.get('generated_at'))
print()
# 1. Pipelines with error/warn status
print("=== PIPELINES (error/warn) ===")
for p in d.get('pipelines', []):
if p.get('status') in ('error', 'fail', 'warn'):
print(f"[{p['status']}] {p.get('name') or p.get('script')} | profile={p.get('profile')} | type={p.get('type')} | schedule={p.get('schedule')} | last_run={p.get('last_run')}")
print()
print("=== FEATURE TREE (non-ok nodes) ===")
def walk(n, path=''):
label = n.get('label', '?')
p = f"{path}/{label}"
if n.get('status') not in ('ok', None):
print(f"[{n.get('status')}] {p}")
for pipe in n.get('pipes', []):
if pipe.get('status') != 'ok':
print(f" pipe: [{pipe.get('status')}] {pipe.get('name') or pipe.get('script')} | last_run={pipe.get('last_run')}")
for c in n.get('children', []):
walk(c, p)
walk(d.get('feature_tree', {}))
print()
print("=== DATA ENTITIES (orphan/write_only) ===")
for e in d.get('entities', []):
if e.get('flow_status') in ('orphan', 'write_only'):
print(f"[{e['flow_status']}] {e['name']} | rows={e.get('rows')} | writers={e.get('writers')} | readers={e.get('readers')} | {e.get('desc','')[:60]}")
print()
print("=== ARCHITECTURE violations ===")
arch = d.get('architecture', {})
print("violation_count:", arch.get('violation_count'))
for v in (arch.get('price_api_violations') or [])[:10]:
print(f" {v.get('script')} L{v.get('line')}")
print()
print("=== JSON files with warn ===")
for j in d.get('json_files', []):
if j.get('warn'):
print(f"[warn] {j['name']} | {j.get('desc','')[:60]} | readers={j.get('readers')}")
+31
View File
@@ -0,0 +1,31 @@
import os
files = ['meta_growth.py', 'macro_context_collector.py', 'divergence_detector.py',
'memory_guardian.py', 'fix_gateway_port.py']
d = '/home/hmo/.hermes/profiles/position-analyst/scripts/'
for f in files:
p = os.path.join(d, f)
if os.path.islink(p):
target = os.readlink(p)
ok = os.path.exists(p)
print(f"{f} -> symlink -> {target} [{'OK' if ok else 'BROKEN'}]")
elif os.path.exists(p):
print(f"{f} -> real file")
else:
print(f"{f} -> MISSING")
print()
print('=== deploy/profile-scripts candidates ===')
deploy = '/home/hmo/MoFin/deploy/profile-scripts/'
for f in os.listdir(deploy):
if any(k in f for k in ['meta_growth', 'macro_context', 'divergence', 'memory_guardian', 'fix_gateway']):
print(' ', f)
print()
print('=== search whole MoFin for the 5 scripts ===')
import subprocess
for f in files:
r = subprocess.run(['find', '/home/hmo/MoFin', '/home/hmo/projects', '-name', f,
'-not', '-path', '*/venv/*'], capture_output=True, text=True, timeout=30)
found = [l for l in r.stdout.splitlines() if l.strip()]
print(f"{f}: {found if found else 'NOT FOUND'}")
+31
View File
@@ -0,0 +1,31 @@
import json
from datetime import datetime, timezone, timedelta
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
now = datetime.now(timezone.utc)
print(f"{'name':32} {'last_run':20} {'status':8} {'next_run':20} {'en'}")
print('-' * 95)
for j in sorted(jobs, key=lambda x: x.get('last_run_at') or ''):
if not j.get('enabled', True):
continue
lr = (j.get('last_run_at') or '?')[:19]
nr = (j.get('next_run_at') or '?')[:19]
st = str(j.get('last_status', '?'))
print(f"{j.get('name','?')[:32]:32} {lr:20} {st:8} {nr:20} {j.get('enabled')}")
# also check gateway-side cron (default profile)
print()
print('=== default profile cron ===')
try:
d2 = json.load(open('/home/hmo/.hermes/cron/jobs.json'))
jobs2 = d2 if isinstance(d2, list) else d2.get('jobs', [])
for j in sorted(jobs2, key=lambda x: x.get('last_run_at') or ''):
if not j.get('enabled', True):
continue
lr = (j.get('last_run_at') or '?')[:19]
nr = (j.get('next_run_at') or '?')[:19]
st = str(j.get('last_status', '?'))
print(f"{j.get('name','?')[:32]:32} {lr:20} {st:8} {nr:20} {j.get('enabled')}")
except Exception as e:
print('err:', e)
+15
View File
@@ -0,0 +1,15 @@
import json, os, glob
# find job ids for the failing jobs
targets = {
'pa': ('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json',
['大脑任务执行', 'Gateway看门狗-知微', '元自成长-每日', '记忆守卫-每日', '宏观新闻采集', '跨市场背离检测']),
'default': ('/home/hmo/.hermes/cron/jobs.json',
['大脑任务执行', 'evolution-pulse', 'wiki-self-growth', '知识研究-日常', '梦境循环-知识库归并']),
}
for label, (jf, names) in targets.items():
d = json.load(open(jf))
jobs = d if isinstance(d, list) else d.get('jobs', [])
for j in jobs:
if j.get('name') in names:
print(f"{label} | {j['name']} | id={j.get('id')} | status={j.get('last_status')} | error={str(j.get('last_error'))[:200]}")
+33
View File
@@ -0,0 +1,33 @@
import os, sys
scripts_dir = '/home/hmo/.hermes/profiles/position-analyst/scripts'
fixed, skipped, errors = 0, 0, []
for name in os.listdir(scripts_dir):
p = os.path.join(scripts_dir, name)
if not os.path.islink(p):
skipped += 1
continue
target = os.path.realpath(p)
if not os.path.exists(target):
errors.append(f"{name}: broken symlink -> {target}")
continue
try:
os.unlink(p)
os.link(target, p) # hardlink: same inode, .resolve() stays in scripts_dir
fixed += 1
except Exception as e:
errors.append(f"{name}: {e}")
print(f"fixed: {fixed}, skipped (non-symlink): {skipped}, errors: {len(errors)}")
for e in errors:
print(' ', e)
# verify with the scheduler's own check
from pathlib import Path
scripts_resolved = Path(scripts_dir).resolve()
test = (Path(scripts_dir) / 'meta_growth.py').resolve()
try:
test.relative_to(scripts_resolved)
print('VERIFY OK: meta_growth.py now passes relative_to check')
except ValueError:
print('VERIFY FAIL: still resolves outside')
+22
View File
@@ -0,0 +1,22 @@
import ast, sys, json
p = '/home/hmo/MoFin/xmpp_logger.py'
try:
ast.parse(open(p).read())
print('SYNTAX OK')
except SyntaxError as e:
print('SYNTAX ERROR:', e)
sys.exit(1)
sys.path.insert(0, '/home/hmo/MoFin')
import importlib, xmpp_logger
importlib.reload(xmpp_logger)
x = xmpp_logger
import time
print('== current_provider(zhiwei):', x.current_provider('zhiwei'))
print('== current_provider(default):', x.current_provider('default'))
print('== scan zhiwei:', json.dumps(x._scan_agent_log(time.time(), 'zhiwei'), ensure_ascii=False))
print('== scan default:', json.dumps(x._scan_agent_log(time.time(), 'default'), ensure_ascii=False))
print()
print('== auto_heal ==')
print(json.dumps(x.auto_heal(), ensure_ascii=False, indent=2))
+37
View File
@@ -0,0 +1,37 @@
import json, urllib.request
from pathlib import Path
print("=== 验证1: 5个被 Blocked 的脚本现在是否通过调度器路径检查 ===")
scripts_dir = Path('/home/hmo/.hermes/profiles/position-analyst/scripts')
resolved = scripts_dir.resolve()
files = ['meta_growth.py', 'macro_context_collector.py', 'divergence_detector.py',
'memory_guardian.py', 'fix_gateway_port.py']
all_ok = True
for f in files:
p = (scripts_dir / f).resolve()
try:
p.relative_to(resolved)
exists = p.exists()
print(f" PASS {f} (exists={exists})")
if not exists:
all_ok = False
except ValueError:
all_ok = False
print(f" FAIL {f} still blocked")
print(' =>', 'ALL PASS' if all_ok else 'STILL FAILING')
print()
print("=== 验证2: default gateway (8642) LLM 调用(原 key1 429,现 key6===")
payload = json.dumps({'model': 'deepseek-v4-flash',
'messages': [{'role': 'user', 'content': 'reply with one word: ok'}],
'max_tokens': 10}).encode()
req = urllib.request.Request('http://127.0.0.1:8642/v1/chat/completions', data=payload,
headers={'Content-Type': 'application/json',
'Authorization': 'Bearer hermes123'})
try:
resp = urllib.request.urlopen(req, timeout=90)
d = json.loads(resp.read().decode())
content = d.get('choices', [{}])[0].get('message', {}).get('content', '')
print(' LLM OK:', content[:100])
except Exception as e:
print(' LLM FAIL:', e)