merge: deploy-time auto hardlink repair

This commit is contained in:
知微
2026-07-20 20:27:17 +08:00
6 changed files with 217 additions and 1 deletions
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Watch deploy/profile-scripts for changes and re-sync hardlinks
[Path]
PathChanged=/home/hmo/MoFin/deploy/profile-scripts
Unit=profile-scripts-sync.service
[Install]
WantedBy=multi-user.target
+7
View File
@@ -0,0 +1,7 @@
[Unit]
Description=Re-sync profile scripts hardlinks after deploy dir changes
[Service]
Type=oneshot
User=hmo
ExecStart=/bin/bash /home/hmo/MoFin/deploy/profile-scripts/sync_profile_scripts.sh
+5 -1
View File
@@ -13,7 +13,11 @@
3. **不可见即不存在** — 组件不在 Dashboard 中显示 = 等于没部署。离线不告警 = 监控缺陷
4. **实现后同步 Spec** — 每轮开发完毕后,必须将 `specs/{module}.json` 更新为与实际实现一致的状态。文档过期 = 等于没写
5. **部署目标即验收标准** — 所有代码必须以部署目标环境(Linux 246)为基准编写和测试。禁止使用 Windows 专属 API`tasklist``netstat``schtasks``wmic`)在 246 部署的代码中
6. **单一事实源(SSOT** — 每个文件全系统只有一个权威位置,其他位置只允许硬链接(同 inode),**禁止独立副本**。权威位置:`deploy/profile-scripts/`cron 脚本)、`/home/hmo/MoFin/`(被 import 的库)、`deploy/bot/`XMPP bot)。修改任何文件后若存在硬链接关系被破坏(scp/编辑器换 inode),必须立即跑 `deploy/profile-scripts/sync_profile_scripts.sh` 重建
6. **单一事实源(SSOT** — 每个文件全系统只有一个权威位置,其他位置只允许硬链接(同 inode),**禁止独立副本**。权威位置:`deploy/profile-scripts/`cron 脚本)、`/home/hmo/MoFin/`(被 import 的库)、`deploy/bot/`XMPP bot)。硬链接破坏只会发生在部署动作(scp 替换文件 / git checkout/merge),因此部署链路自带三层自动修复,不需要靠记性:
- **systemd path watcher**`profile-scripts-sync.path`):监听 `deploy/profile-scripts/` 目录变化,任何 scp/替换自动触发 `sync_profile_scripts.sh`
- **git hooks**`.git/hooks/post-merge` + `post-checkout`):git 操作后自动重链
- **手动兜底**`bash deploy/profile-scripts/sync_profile_scripts.sh`(改完文件随手跑)
- 同步日志:`gateway/logs/link_sync.log`;断链检测同时是 L2 卫生审计的检查项
7. **数据路径必须绝对** — 引用数据文件/数据库时,必须写绝对路径并指向权威位置(`/home/hmo/MoFin/data/`)。**禁止**用 `Path(__file__).parent / "data"` 这类相对解析——同一个模块被硬链接到不同位置时会解析出不同的数据库(2026-07-20 三库事件的根因)
8. **备份/遗留物禁止留在生产数据目录**`.bak``decisions_backup_*`、迁移残留 JSON、废弃 DB,必须在迁移/变更完成时移到 `archive/`。生产数据目录(`MoFin/data` = `web-dashboard/data`)只放活文件。监控脚本扫描生产目录时,遗留物就是未来的假警报
9. **死模块必须收尸** — 宣布模块废弃时,必须在同一轮操作中完成收尸六步:①杀进程 ②stop+disable systemd 服务 ③删 cron job ④归档脚本到 `archive/` ⑤归档数据文件 ⑥从期望矩阵/监控中移除。只说"已废弃"不收尸 = 没废弃(小果 bot 以 root 白跑 8 天 2.5GB 的教训)
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# git post-merge / post-checkout hook — 自动重建硬链接
# git 操作(merge/checkout/rebase)会替换文件、破坏硬链接,钩子自动修复
REPO="/home/hmo/MoFin"
if [ -x "$REPO/deploy/profile-scripts/sync_profile_scripts.sh" ]; then
bash "$REPO/deploy/profile-scripts/sync_profile_scripts.sh" >/dev/null 2>&1
echo "$(date '+%F %T') auto-synced by git $1 hook" >> "$REPO/gateway/logs/link_sync.log"
fi
exit 0
+69
View File
@@ -0,0 +1,69 @@
import json, urllib.request
msg = """[笑笑] 自检体系 L0-L4 分层架构 — 建成并验证
按你的四点要求重构完成:
■ 责任矩阵(每层职责单一,不重叠不疏漏)
L0 执行心跳 — agents_health_check.py5min
职责:端口/HTTP/DB 存活 + auto_heal 执行(gateway重启/key切换/bot重启)
L1 功能健康 — functional_health_check.py(交易时段15min
职责:判据是"功能是否达成"不是"进程活着"。9个核心模块各有注册判据:
· price_monitor → live_prices 最新记录 <6min
· market_watch → market_snapshots <40min
· mtf_cache → <75min
· macro_context → <45min
· 健康采集 → mofin_health.json <20min
· 盘前重评 → summary <26h
· LLM → agent.log 有成功调用
· bot → systemctl active + 无断线循环
· cron引擎 → 最近12min有job运行
L2 系统卫生 — system_hygiene_audit.py(每日08:20,已从每周改每日)
职责:分叉副本/断裂硬链接/僵尸进程/孤儿文件/死cron/DB新鲜度
L3 LLM修复循环 — self_repair.py30min
职责:读 L1/L2 失败项 → LLM 诊断 → 直接修复(报备制)
L4 元监控 — meta_watchdog.py(每小时)
职责:自检系统的自检——L0-L3 输出物新鲜度 + L3注册状态 + XMPP桥。
任何一层死亡直接 XMPP 点名(最后兜底)
已退休重叠组件:Cron监护-高频(并入L3)、全局cron健康监控(并入L1)
■ 报备制(不是报告了事)
L3 的纪律:发现问题 → 直接修 → 记录 repair_log.jsonl → XMPP 报备。
LLM 只许选白名单动作(rerun_script/restart_service/sync_links/switch_llm_key/none),
不许任意执行代码;每模块每天≤2次防修复循环;白名单兜不住会明说"需要人工"
■ 端到端验证(真实测试)
1. 我手动 stop xmpp bot → L1 立即报 failbot 服务非 active
2. systemd Restart=always 自动拉起 → L3 LLM 诊断后正确判定"无需动作"并记录日志
3. rerun_script 白名单路径实测:真实重跑 mofin_health.py 成功
4. meta_watchdog 复检:L0-L4 全绿
5. 第一版 bot 判据被我自己抓出 bug(信30分钟前日志不看当前状态),已修正为"先看 is-active"
■ F健康整合
mofin_health.html 新增 🩺自检体系 Tab(在 功能树/全部Cron/数据实体/数据流 之后):
· L4 元监控各层状态表
· L1 九项功能判据逐项状态
· L2 卫生问题清单
· L3 最近修复动作时间线(报备记录)
你打开 Dashboard → 健康 → 🩺自检体系 就能看到系统各方各面的健康状态,
以及自检系统自身的运转情况。责任矩阵已写入 dev-spec.md 第三章。
代码已提交合并(246: e69109fd)。"""
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"})
try:
print("XMPP:", urllib.request.urlopen(req, timeout=10).read().decode()[:80])
except Exception as e:
print("XMPP fail:", e)
+118
View File
@@ -0,0 +1,118 @@
import json, os, subprocess, hashlib
from datetime import datetime
now = datetime.now()
ok_count = 0
fail_items = []
def check(name, ok, detail=''):
global ok_count
icon = '' if ok else ''
print(f'{icon} {name}: {detail}')
if ok:
ok_count += 1
else:
fail_items.append(name)
def md5(p):
try:
return hashlib.md5(open(p, 'rb').read()).hexdigest()[:10]
except Exception:
return 'ERR'
print('=== 1. 新脚本文件部署(deploy + profile scripts 硬链接一致)===')
NEW_SCRIPTS = ['functional_health_check.py', 'self_repair.py', 'meta_watchdog.py',
'system_hygiene_audit.py', 'sync_profile_scripts.sh', 'mofin_health.py']
for f in NEW_SCRIPTS:
dp = f'/home/hmo/MoFin/deploy/profile-scripts/{f}'
pp = f'/home/hmo/.hermes/profiles/position-analyst/scripts/{f}'
d_ok = os.path.exists(dp)
p_ok = os.path.exists(pp)
same = d_ok and p_ok and md5(dp) == md5(pp)
check(f, same, f'deploy={d_ok} profile={p_ok} 内容一致={same}')
print()
print('=== 2. cron 注册(L1/L2/L3/L4 jobs===')
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
expect = {
'functional_health_check.py': 'L1功能健康(15min)',
'self_repair.py': 'L3修复循环(30min)',
'meta_watchdog.py': 'L4元监控(每小时)',
'system_hygiene_audit.py': 'L2卫生(每日)',
}
for script, label in expect.items():
j = next((x for x in jobs if x.get('script') == script), None)
if not j:
check(label, False, '未注册')
else:
sched = j.get('schedule_display') or str(j.get('schedule'))
en = j.get('enabled', True)
check(label, bool(j and en), f'registered sched={sched} enabled={en}')
# 退休的 job 不在
for dead in ['Cron监护-高频', '全局cron健康监控-每10分']:
j = next((x for x in jobs if x.get('name') == dead), None)
check(f'退休job[{dead}]已删除', j is None, '已删除' if j is None else '仍存在!')
print()
print('=== 3. 输出物生成 ===')
for name, path, max_h in [
('functional_health.json', '/home/hmo/MoFin/gateway/logs/functional_health.json', 1),
('hygiene_report.json', '/home/hmo/MoFin/gateway/logs/hygiene_report.json', 26),
('meta_watchdog.json', '/home/hmo/MoFin/gateway/logs/meta_watchdog.json', 2),
('mofin_health.json', '/home/hmo/web-dashboard/static/mofin_health.json', 1),
]:
if not os.path.exists(path):
check(name, False, '不存在')
else:
age_h = (now.timestamp() - os.path.getmtime(path)) / 3600
check(name, age_h < max_h, f'{age_h:.1f}h前生成(阈值{max_h}h')
print()
print('=== 4. mofin_health.json 含 self_check 区块 ===')
try:
mh = json.load(open('/home/hmo/web-dashboard/static/mofin_health.json'))
sc = mh.get('self_check', {})
check('self_check.functional', bool(sc.get('functional')), str(bool(sc.get('functional'))))
check('self_check.meta_watchdog', bool(sc.get('meta_watchdog')), str(bool(sc.get('meta_watchdog'))))
check('self_check.hygiene', bool(sc.get('hygiene')), str(bool(sc.get('hygiene'))))
except Exception as e:
check('mofin_health.json', False, str(e))
print()
print('=== 5. Dashboard 页面含自检体系 Tab ===')
html = open('/home/hmo/web-dashboard/static/mofin_health.html', encoding='utf-8').read()
check('🩺自检体系 tab', '自检体系' in html and 'panel4' in html, '')
r = subprocess.run(['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}',
'http://127.0.0.1:8899/mofin_health.html'], capture_output=True, text=True, timeout=10)
check('dashboard 页面 200', r.stdout.strip() == '200', f'HTTP {r.stdout.strip()}')
print()
print('=== 6. systemd 超时配置 ===')
r1 = subprocess.run(['sudo', '-n', 'systemctl', 'show', 'hermes-gateway-zhiwei', '-p', 'Environment'],
capture_output=True, text=True, timeout=10)
check('zhiwei HERMES_CRON_SCRIPT_TIMEOUT=600', 'HERMES_CRON_SCRIPT_TIMEOUT=600' in r1.stdout, '')
r2 = subprocess.run(['systemctl', '--user', 'show', 'hermes-gateway', '-p', 'Environment'],
capture_output=True, text=True, timeout=10)
check('default HERMES_CRON_SCRIPT_TIMEOUT=600', 'HERMES_CRON_SCRIPT_TIMEOUT=600' in r2.stdout, '')
print()
print('=== 7. 小果残留 ===')
r = subprocess.run(['ps', 'aux'], capture_output=True, text=True, timeout=10)
xg = [l for l in r.stdout.splitlines() if 'xiaoguo' in l.lower() and 'grep' not in l]
check('小果进程清零', len(xg) == 0, f'{len(xg)}个残留' if xg else '')
r = subprocess.run(['ss', '-tlnp'], capture_output=True, text=True, timeout=10)
check('8645 已关闭', '8645' not in r.stdout, '')
print()
print('=== 8. 服务全景 ===')
r = subprocess.run(['ss', '-tlnp'], capture_output=True, text=True, timeout=10)
for port, label in [('8642', 'default gw'), ('8643', 'zhiwei gw'), ('8646', 'mohe gw'),
('8899', 'dashboard'), ('5222', 'ejabberd'), ('5805', 'xmpp桥')]:
check(f'{label}:{port}', f':{port}' in r.stdout, '')
print()
print(f'=== 汇总: {ok_count} 项通过, {len(fail_items)} 项失败 ===')
if fail_items:
print('失败项:', fail_items)