feat(L2): 卫生审计自动收尸——scripts/影子副本+零引用孤儿(mtime>7天)自动git mv归档并提交; dev-spec新增文件居住宪法(红线#9)

This commit is contained in:
hmo
2026-07-22 08:41:06 +08:00
parent 375f6d469a
commit 3ea7c52112
2 changed files with 86 additions and 1 deletions
@@ -62,6 +62,88 @@ def md5(p):
return 'ERR'
def _collect_live_refs():
"""收集活引用:cron jobs.json 脚本名 + 活代码 import/字符串路径引用"""
import re as _re
refs = set()
for pj in glob.glob('/home/hmo/.hermes/profiles/*/cron/jobs.json'):
try:
with open(pj, encoding='utf-8') as f:
jobs = json.load(f)
jobs = jobs if isinstance(jobs, list) else jobs.get('jobs', [])
for j in jobs:
if j.get('enabled', True) and j.get('script'):
refs.add(os.path.basename(j['script']))
except Exception:
pass
scan = [f'{MOFIN_ROOT}/server.py', f'{MOFIN_ROOT}/xmpp_logger.py']
scan += glob.glob(f'{DEPLOY}/*.py')
scan += glob.glob(f'{MOFIN_ROOT}/deploy/bot/*.py')
scan += glob.glob(f'{MOFIN_ROOT}/prompt_manager/*.py')
for sf in scan:
try:
with open(sf, encoding='utf-8', errors='replace') as f:
c = f.read()
for m in _re.finditer(r'(?:from|import)\s+([a-zA-Z0-9_]+)', c):
refs.add(m.group(1) + '.py')
for m in _re.finditer(r'["\']([a-zA-Z0-9_\-]+\.py)["\']', c):
refs.add(m.group(1))
except Exception:
pass
return refs
def auto_archive_orphans(now):
"""源头自动化收尸(2026-07-22 老爸批准):
scripts/ 下的 ①影子副本(deploy同名) ②零引用孤儿,且 mtime>7天 →
自动 git mv 到 archive/YYYYMM-auto/ 并自动提交(GIT_ALLOW_COMMIT)。
返回归档记录列表。7天内新文件不动(防误收在途工作)。"""
archived = []
scripts_dir = f'{MOFIN_ROOT}/scripts'
if not os.path.isdir(scripts_dir):
return archived
cutoff = (now - timedelta(days=7)).timestamp()
refs = _collect_live_refs()
dest_rel = f'archive/{now.strftime("%Y%m")}-auto'
to_move = []
for f in sorted(os.listdir(scripts_dir)):
if not f.endswith('.py'):
continue
p = os.path.join(scripts_dir, f)
if os.path.getmtime(p) > cutoff:
continue
if os.path.exists(os.path.join(DEPLOY, f)):
to_move.append((f, 'shadow'))
elif f not in refs:
to_move.append((f, 'orphan'))
if not to_move:
return archived
os.makedirs(f'{MOFIN_ROOT}/{dest_rel}', exist_ok=True)
env = dict(os.environ)
env['GIT_ALLOW_COMMIT'] = '1'
for f, why in to_move:
r = subprocess.run(['git', '-C', MOFIN_ROOT, 'mv', f'scripts/{f}', f'{dest_rel}/{f}'],
capture_output=True, text=True, env=env, timeout=30)
if r.returncode != 0:
try:
os.rename(f'{scripts_dir}/{f}', f'{MOFIN_ROOT}/{dest_rel}/{f}')
r = subprocess.run(['git', '-C', MOFIN_ROOT, 'add', f'{dest_rel}/{f}'],
capture_output=True, env=env, timeout=30)
except Exception as e:
print(f' ⚠️ 归档失败 {f}: {e}', flush=True)
continue
archived.append({'type': 'auto_archive', 'file': f'scripts/{f}',
'action': f'已自动归档({why}) → {dest_rel}/'})
if archived:
subprocess.run(['git', '-C', MOFIN_ROOT, 'add', '-A', 'archive/', 'scripts/'],
capture_output=True, env=env, timeout=30)
subprocess.run(['git', '-C', MOFIN_ROOT, 'commit', '-m',
f'chore: L2卫生自动归档 {len(archived)} 个影子/孤儿文件({dest_rel}'],
capture_output=True, env=env, timeout=60)
print(f' 🧹 自动归档 {len(archived)} 个文件 → {dest_rel}/', flush=True)
return archived
def check_diverged():
"""检查 deploy vs MoFin/scripts vs MoFin根 的分叉副本"""
issues = []
@@ -292,6 +374,8 @@ def check_db_freshness():
def main():
print('🧹 系统卫生审计', datetime.now().strftime('%Y-%m-%d %H:%M'))
all_issues = []
# 源头自动化:先把影子/孤儿归档,再跑检查(检查看到的应是归档后的干净状态)
all_issues.extend(auto_archive_orphans(datetime.now()))
for name, fn in [('分叉副本', check_diverged), ('断裂硬链接', check_broken_hardlinks),
('僵尸进程', check_zombies), ('孤儿文件', check_orphan_files),
('死cron', check_dead_cron), ('DB新鲜度', check_db_freshness),
+2 -1
View File
@@ -21,7 +21,8 @@
- **部署守卫**`deploy_guard.py`,每 15 分钟全天候):①被跟踪代码文件出现未提交改动 → 自动回滚+重链+告警(git 提交不受影响)②`session-work` 领先 master 且可快进 → 自动 merge+重链+(触及 dashboard 时)重启服务 ③幂等重链 ④cron 引用完整性检查。状态落盘 `gateway/logs/deploy_guard_status.json`,有动作即 XMPP 报备。**禁止直接编辑 246 上任何被跟踪的代码文件**——所有改动必须经 git(详见 `docs/zhiwei-ops-discipline.md`
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. **文件居住宪法(源头防副本)** — 新文件只允许落在三处 canonical:cron 脚本 → `deploy/profile-scripts/`;被 import 的库 → repo 根目录;独立服务 → `deploy/bot/`。**禁止**在 `scripts/` 下新建 cron 脚本副本或库副本(`scripts/` 只放被系统引用的工具)。一次性诊断/修复脚本 → 用完即归档 `archive/YYYYMMDD-*/` 或放 `temp/`。违者由 L2 卫生审计的**自动收尸**处理:`scripts/` 下的影子副本(deploy 同名)和零引用孤儿(mtime>7天)会被自动 `git mv``archive/YYYYMM-auto/` 并自动提交——不需要人工清理,也不会再堆积(2026-07-22 起,scripts/ 从 186 个文件收尸到个位数
10. **死模块必须收尸** — 宣布模块废弃时,必须在同一轮操作中完成收尸六步:①杀进程 ②stop+disable systemd 服务 ③删 cron job ④归档脚本到 `archive/` ⑤归档数据文件 ⑥从期望矩阵/监控中移除。只说"已废弃"不收尸 = 没废弃(小果 bot 以 root 白跑 8 天 2.5GB 的教训)
10. **监控查"活"不查"在"** — 健康检查必须验证**数据新鲜度**(DB 表 MAX(时间列))而非"文件存在/进程存在"。文件 mtime、进程存活都不构成健康证据——数据 24h 不更新才是事故。禁止拿遗留文件的 mtime 当管道健康指标("数据管道停滞14天"假警报的根因)
11. **批量 LLM 调用禁止走 hermes gateway agent 通道** — hermes gateway 的 `/v1/chat/completions` **不是透传**,是完整 agent 运行时:每个请求创建带工具(terminal/websearch/patch)的 agent 会话,可能螺旋几十轮、累积 150k+ token,客户端超时后服务端仍空转,重试会叠加新会话形成自我 DDoS2026-07-21 603288 事件:单次重评螺旋 35 分钟、44 次 terminal 调用)。所有批量/脚本化 LLM 调用必须经 `llm_client.call_llm()`:**OCG 上游直连为主**(裸 completionkey 运行时从 hermes config.yaml 读取,不落盘),gateway 仅作应急兜底。新增 LLM 调用点一律复用 `llm_client`,禁止手写 HTTP 调用
12. **告警信噪比纪律** — 所有系统 XMPP 告警必须经 `alert_helper.notify()`,禁止直 POST :5805。两级通道:**ACTION**(买入信号/重点推荐/需人工核查)直通不限速、🚨 前缀独立成条,**永不被限速**;**INFO**(部署/卫生/修复/监控报备)同类 30 分钟限 1 条、≤8 行、24h 内容去重(同一问题不重复轰炸)。有意义的信号(重点推荐操作)绝不允许被纯通知淹没;通知型信息零问题 = 零消息(沉默即正常)