import os, shutil, glob from datetime import datetime ARCHIVE = f'/home/hmo/MoFin/archive/dedup-20260720-{datetime.now().strftime("%H%M")}' os.makedirs(ARCHIVE, exist_ok=True) def hardlink_to(src, targets): """把 targets 全部硬链接到 src(内容统一为 src)""" done = [] for t in targets: if not os.path.exists(t) and not os.path.islink(t): continue if os.path.isfile(t) and os.path.samefile(src, t): continue try: if os.path.islink(t) or os.path.isfile(t): os.unlink(t) os.link(src, t) done.append(t) except Exception as e: print(f' LINK FAIL {t}: {e}') return done def archive(path, note=''): if os.path.exists(path) or os.path.islink(path): dst = os.path.join(ARCHIVE, os.path.basename(path)) if os.path.exists(dst): dst = dst + '.' + datetime.now().strftime('%H%M%S') shutil.move(path, dst) print(f' archived: {path} {note}') # ── A. LIBRARY 类:root 是活的(cron 经 sys.path 导入),统一所有副本到 root ── LIBRARY = ['strategy_lifecycle.py', 'strategy_tree.py', 'technical_analysis.py', 'multi_timeframe.py', 'mofin_news.py'] print('=== A. LIBRARY 统一到 MoFin root ===') for f in LIBRARY: root = f'/home/hmo/MoFin/{f}' if not os.path.exists(root): print(f' {f}: root 不存在,跳过!') continue targets = [ f'/home/hmo/MoFin/deploy/profile-scripts/{f}', f'/home/hmo/.hermes/profiles/position-analyst/scripts/{f}', f'/home/hmo/MoFin/scripts/{f}', f'/home/hmo/web-dashboard/{f}', ] done = hardlink_to(root, targets) print(f' {f}: unified {len(done)} copies -> root') # ── B. CRON 类:deploy 是唯一源,归档其他副本 ── CRON_FILES = ['advice_reconciliation.py', 'branch_scanner.py', 'collect_evaluation_data.py', 'cron_to_xmpp.py', 'market_insight.py', 'memory_guardian.py', 'mofin_health.py', 'premarket_full_review.py', 'promote_candidates.py', 'prune_branches.py', 'strategy_evaluator.py', 'system_health_check.py', 'system_audit.py'] print('\n=== B. CRON 类归档非 deploy 副本 ===') for f in CRON_FILES: for loc in ['/home/hmo/MoFin', '/home/hmo/MoFin/scripts', '/home/hmo/web-dashboard', '/home/hmo/.hermes/scripts']: p = os.path.join(loc, f) # 不碰 deploy 和 pa/scripts(硬链接到 deploy) if os.path.exists(p) or os.path.islink(p): # 跳过与 deploy 同 inode 的(那些是合法硬链接) deploy_p = f'/home/hmo/MoFin/deploy/profile-scripts/{f}' if os.path.exists(deploy_p) and os.path.isfile(p) and not os.path.islink(p): try: if os.path.samefile(p, deploy_p): continue except Exception: pass archive(p, f'(cron file {f})') # ── C. market_watch / market_screener 特殊:crontab 正在跑 root 旧版 ── print('\n=== C. market_watch/market_screener 统一 ===') # 暂时只统一内容到 deploy(deploy 是 hermes cron 维护的版本),crontab 稍后改指向 for f in ['market_watch.py', 'market_screener.py']: deploy_p = f'/home/hmo/MoFin/deploy/profile-scripts/{f}' if os.path.exists(deploy_p): done = hardlink_to(deploy_p, [f'/home/hmo/MoFin/{f}', f'/home/hmo/MoFin/scripts/{f}']) print(f' {f}: unified {len(done)} -> deploy (crontab 路径仍有效但内容已统一)') # ── D. 待查类 ── print('\n=== D. 待查类 ===') # server.py: web-dashboard + root 是活的 dashboard,归档 deploy 旧拷贝 archive('/home/hmo/MoFin/deploy/profile-scripts/server.py', '(live=web-dashboard hardlink)') # xmpp_agent_core.py: deploy/bot 是活的 for p in ['/home/hmo/MoFin/xmpp_agent_core.py', '/home/hmo/MoFin/scripts/xmpp_agent_core.py', '/home/hmo/.hermes/scripts/xmpp_agent_core.py', '/home/hmo/web-dashboard/xmpp_agent_core.py']: archive(p, '(live=deploy/bot)') # 手动工具类:deploy 为准,归档 root 旧副本 for f in ['mo_provider.py', 'mofin_collect.py', 'stock_profile.py', 'stock_sector_enrich.py', 'trend_detector.py', 'bulk_strategy_regenerate.py', 'strategy_feedback.py']: deploy_p = f'/home/hmo/MoFin/deploy/profile-scripts/{f}' if os.path.exists(deploy_p): done = hardlink_to(deploy_p, [f'/home/hmo/MoFin/{f}', f'/home/hmo/MoFin/scripts/{f}']) print(f' {f}: unified {len(done)} -> deploy') else: for loc in ['/home/hmo/MoFin', '/home/hmo/MoFin/scripts']: archive(os.path.join(loc, f), '(no deploy version)') print('\n=== 验证:关键 import 测试 ===') import subprocess r = subprocess.run( "cd /home/hmo/.hermes/profiles/position-analyst/scripts && python3 -c \"import sys; sys.path.insert(0,'/home/hmo/MoFin'); import strategy_lifecycle, technical_analysis, multi_timeframe, mofin_news, strategy_tree; print('imports OK', strategy_lifecycle.__file__)\"", shell=True, capture_output=True, text=True, timeout=30) print(r.stdout.strip() or r.stderr.strip()[:200]) print('\nARCHIVE dir:', ARCHIVE)