文件预览

teardown.py

查看 YouOS 技能包中的文件内容。

文件内容

scripts/teardown.py

#!/usr/bin/env python3
"""YouOS teardown — remove all user data while keeping the code."""

from __future__ import annotations

import argparse
import shutil
import sqlite3
import sys
from pathlib import Path

ROOT_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT_DIR))

# Resolve instance paths via settings so teardown honors YOUOS_DATA_DIR —
# without this, running `scripts/teardown.py` with an instance set would
# print "delete this" pointing at the repo dirs while the actual user data
# (DB, adapters, var/) sat untouched in the instance, and the rmtree below
# would scrub the repo's own dev artefacts instead.
from app.core.settings import get_instance_root, get_models_dir, get_var_dir  # noqa: E402
from app.db.bootstrap import resolve_sqlite_path  # noqa: E402


def _dir_size(path: Path) -> str:
    """Human-readable directory size."""
    if not path.exists():
        return "0 B"
    total = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
    for unit in ("B", "KB", "MB", "GB"):
        if total < 1024:
            return f"{total:.1f} {unit}"
        total /= 1024
    return f"{total:.1f} TB"


def _file_size(path: Path) -> str:
    if not path.exists():
        return "0 B"
    size = path.stat().st_size
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024:
            return f"{size:.1f} {unit}"
        size /= 1024
    return f"{size:.1f} TB"


def _feedback_count(db_path: Path) -> int:
    if not db_path.exists():
        return 0
    try:
        conn = sqlite3.connect(db_path)
        count = conn.execute("SELECT COUNT(*) FROM feedback_pairs").fetchone()[0]
        conn.close()
        return count
    except Exception:
        return 0


def _stop_server() -> None:
    """Stop any running uvicorn server."""
    import subprocess

    try:
        subprocess.run(
            ["pkill", "-f", "uvicorn.*app.main:app"],
            capture_output=True,
            timeout=5,
        )
        print("  Stopped running server.")
    except Exception:
        pass


def _remove_cron() -> None:
    """Remove nightly cron job if OpenClaw is available."""
    import subprocess

    try:
        if shutil.which("openclaw"):
            subprocess.run(
                ["openclaw", "cron", "remove", "--name", "youos:nightly"],
                capture_output=True,
                timeout=10,
            )
            print("  Removed nightly cron job.")
    except Exception:
        pass


def teardown(delete_all: bool = False) -> None:
    from app.core.settings import get_settings

    settings = get_settings()
    instance_root = get_instance_root()
    db_path = resolve_sqlite_path(settings.database_url)
    data_dir = instance_root / "data"
    models_dir = get_models_dir()
    var_dir = get_var_dir()

    print()
    print("YouOS Teardown")
    print("=" * 40)
    print(f"Target instance: {instance_root}")
    print()

    # Stop server
    _stop_server()

    # Show what will be deleted
    print("The following will be deleted:")
    print()
    print(f"  Database:        {db_path} ({_file_size(db_path)})")
    print(f"  Raw email cache: {data_dir}/ ({_dir_size(data_dir)})")
    print(f"  LoRA adapters:   {models_dir}/ ({_dir_size(models_dir)})")
    print(f"  Feedback pairs:  {_feedback_count(db_path)} pairs")
    print()

    if not delete_all:
        confirm = input("This will permanently delete your corpus and model. Type 'delete' to confirm: ")
        if confirm.strip().lower() != "delete":
            print("Aborted.")
            return

    # Delete data directories
    removed = []
    for d in [data_dir, models_dir, var_dir]:
        if d.exists():
            shutil.rmtree(d)
            removed.append(str(d))
            print(f"  Removed {d}/")

    # Remove analysis file (in the instance configs, not the repo)
    analysis = Path(settings.configs_dir) / "persona_analysis.json"
    if analysis.exists():
        analysis.unlink()
        print(f"  Removed {analysis}")

    # Optionally remove cron
    _remove_cron()

    print()
    print(f"YouOS data removed. The code remains at {ROOT_DIR}.")


def main() -> None:
    parser = argparse.ArgumentParser(description="Remove all YouOS user data")
    parser.add_argument("--all", action="store_true", help="Delete everything without prompting")
    args = parser.parse_args()
    teardown(delete_all=args.all)


if __name__ == "__main__":
    main()