文件预览

auto_repair.py

查看 🩺 memory-health-check 技能包中的文件内容。

文件内容

scripts/auto_repair.py

#!/usr/bin/env python3
"""Auto-repair - clean orphaned and temp files."""
import argparse
import json
import logging
import re
import sys
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
logger = logging.getLogger("auto_repair")

MEMORY_DIR = Path.home() / ".openclaw" / "workspace" / "memory"
LINK_PATTERN = re.compile(r"\[\[([^\]|#]+)(?:\|[^\]]+)?\]\]")


def get_ds_store_files(base: Path) -> list[Path]:
    """Return all .DS_Store files under base."""
    try:
        return [f for f in base.rglob(".DS_Store") if f.is_file()]
    except Exception:
        return []


def get_empty_files(base: Path) -> list[Path]:
    """Return all empty (.size == 0) markdown files under base."""
    empty = []
    for f in base.rglob("*.md"):
        try:
            if f.is_file() and f.stat().st_size == 0:
                empty.append(f)
        except Exception:
            pass
    return empty


def get_orphan_files(base: Path) -> list[Path]:
    """Return orphan files (no inbound Obsidian [[...]] references)."""
    md_files = [f for f in base.rglob("*.md") if f.is_file()]
    filename_refs: dict[str, set[str]] = {}

    for f in md_files:
        try:
            content = f.read_text(errors="ignore")
            refs = set()
            for match in LINK_PATTERN.finditer(content):
                linked_name = Path(match.group(1).strip()).stem
                refs.add(linked_name)
            filename_refs[f.stem] = refs
        except Exception:
            pass

    orphans = []
    for f in md_files:
        inbound = any(
            f.stem in refs
            for other_stem, refs in filename_refs.items()
            if other_stem != f.stem
        )
        if not inbound:
            orphans.append(f)
    return orphans


def auto_repair(
    remove_ds_store: bool = True,
    remove_empty: bool = True,
    remove_orphans: bool = False,
    dry_run: bool = False,
    verbose: bool = False,
) -> dict:
    """Remove orphaned entries and temp files (user-approved)."""
    if verbose:
        logger.setLevel(logging.DEBUG)

    if not MEMORY_DIR.exists():
        logger.warning("memory directory not found")
        return {"repaired": [], "errors": [], "summary": "No memory directory found"}

    repaired = []
    errors = []

    # .DS_Store cleanup
    if remove_ds_store:
        for f in get_ds_store_files(MEMORY_DIR):
            if dry_run:
                logger.info(f"[dry-run] Would remove .DS_Store: {f}")
                repaired.append(f"[dry-run] Remove: {f}")
            else:
                try:
                    f.unlink()
                    repaired.append(f"Removed .DS_Store: {f}")
                    logger.info(f"Removed: {f}")
                except Exception as e:
                    errors.append(f"Failed to remove {f}: {e}")
                    logger.error(f"Failed: {f}: {e}")

    # Empty file cleanup
    if remove_empty:
        for f in get_empty_files(MEMORY_DIR):
            if dry_run:
                repaired.append(f"[dry-run] Remove empty: {f}")
                logger.info(f"[dry-run] Would remove empty file: {f}")
            else:
                try:
                    f.unlink()
                    repaired.append(f"Removed empty: {f}")
                    logger.info(f"Removed empty file: {f}")
                except Exception as e:
                    errors.append(f"Failed to remove {f}: {e}")

    # Orphan cleanup (disabled by default - needs explicit flag)
    if remove_orphans:
        orphans = get_orphan_files(MEMORY_DIR)
        if dry_run:
            for f in orphans:
                repaired.append(f"[dry-run] Remove orphan: {f}")
                logger.info(f"[dry-run] Would remove orphan: {f}")
        else:
            for f in orphans:
                try:
                    f.unlink()
                    repaired.append(f"Removed orphan: {f}")
                    logger.info(f"Removed orphan: {f}")
                except Exception as e:
                    errors.append(f"Failed to remove orphan {f}: {e}")

    summary = f"Cleaned {len(repaired)} items, {len(errors)} errors"
    logger.info(summary)

    return {
        "repaired": repaired,
        "errors": errors,
        "summary": summary,
    }


def main():
    parser = argparse.ArgumentParser(description="Auto-repair - clean orphaned and temp files")
    parser.add_argument("--dry-run", action="store_true", help="Show what would be done without making changes")
    parser.add_argument("--remove-orphans", action="store_true", help="Also remove orphan files (default: no)")
    parser.add_argument("--no-ds-store", action="store_true", help="Skip .DS_Store removal")
    parser.add_argument("--no-empty", action="store_true", help="Skip empty file removal")
    parser.add_argument("--json", action="store_true", help="Output JSON only")
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
    args = parser.parse_args()

    result = auto_repair(
        remove_ds_store=not args.no_ds_store,
        remove_empty=not args.no_empty,
        remove_orphans=args.remove_orphans,
        dry_run=args.dry_run,
        verbose=args.verbose,
    )

    if args.json:
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        print(f"[auto_repair] {result['summary']}")
        for r in result["repaired"]:
            print(f"  ✓ {r}")
        if result["errors"]:
            print("Errors:")
            for e in result["errors"]:
                print(f"  ✗ {e}")


if __name__ == "__main__":
    main()