mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
285 lines
10 KiB
Python
285 lines
10 KiB
Python
![]() |
#!/usr/bin/env python3
|
|||
|
"""
|
|||
|
UV Cache 清理腳本
|
|||
|
================
|
|||
|
|
|||
|
定期清理 uv cache 以防止磁碟空間不斷增加
|
|||
|
特別針對 Windows 系統「檔案正由另一個程序使用」的問題提供解決方案
|
|||
|
|
|||
|
使用方式:
|
|||
|
python scripts/cleanup_cache.py --size # 查看 cache 大小和詳細資訊
|
|||
|
python scripts/cleanup_cache.py --dry-run # 預覽將要清理的內容(不實際清理)
|
|||
|
python scripts/cleanup_cache.py --clean # 執行標準清理
|
|||
|
python scripts/cleanup_cache.py --force # 強制清理(會嘗試關閉相關程序)
|
|||
|
|
|||
|
功能特色:
|
|||
|
- 智能跳過正在使用中的檔案
|
|||
|
- 提供強制清理模式
|
|||
|
- 詳細的清理統計和進度顯示
|
|||
|
- 支援 Windows/macOS/Linux 跨平台
|
|||
|
"""
|
|||
|
|
|||
|
import subprocess
|
|||
|
import sys
|
|||
|
import argparse
|
|||
|
import shutil
|
|||
|
from pathlib import Path
|
|||
|
import os
|
|||
|
|
|||
|
def get_cache_dir():
|
|||
|
"""取得 uv cache 目錄"""
|
|||
|
# Windows 預設路徑
|
|||
|
if os.name == 'nt':
|
|||
|
return Path.home() / "AppData" / "Local" / "uv"
|
|||
|
# macOS/Linux 預設路徑
|
|||
|
else:
|
|||
|
return Path.home() / ".cache" / "uv"
|
|||
|
|
|||
|
def get_cache_size(cache_dir):
|
|||
|
"""計算 cache 目錄大小"""
|
|||
|
if not cache_dir.exists():
|
|||
|
return 0
|
|||
|
|
|||
|
total_size = 0
|
|||
|
for dirpath, dirnames, filenames in os.walk(cache_dir):
|
|||
|
for filename in filenames:
|
|||
|
filepath = os.path.join(dirpath, filename)
|
|||
|
try:
|
|||
|
total_size += os.path.getsize(filepath)
|
|||
|
except (OSError, FileNotFoundError):
|
|||
|
pass
|
|||
|
return total_size
|
|||
|
|
|||
|
def format_size(size_bytes):
|
|||
|
"""格式化檔案大小顯示"""
|
|||
|
if size_bytes == 0:
|
|||
|
return "0 B"
|
|||
|
|
|||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|||
|
if size_bytes < 1024.0:
|
|||
|
return f"{size_bytes:.1f} {unit}"
|
|||
|
size_bytes /= 1024.0
|
|||
|
return f"{size_bytes:.1f} TB"
|
|||
|
|
|||
|
def run_uv_command(command, check=True):
|
|||
|
"""執行 uv 命令"""
|
|||
|
try:
|
|||
|
result = subprocess.run(
|
|||
|
["uv"] + command,
|
|||
|
capture_output=True,
|
|||
|
text=True,
|
|||
|
check=check
|
|||
|
)
|
|||
|
return result
|
|||
|
except subprocess.CalledProcessError as e:
|
|||
|
print(f"❌ 命令執行失敗: uv {' '.join(command)}")
|
|||
|
print(f"錯誤: {e.stderr}")
|
|||
|
return None
|
|||
|
except FileNotFoundError:
|
|||
|
print("❌ 找不到 uv 命令,請確認 uv 已正確安裝")
|
|||
|
return None
|
|||
|
|
|||
|
def show_cache_info():
|
|||
|
"""顯示 cache 資訊"""
|
|||
|
print("🔍 UV Cache 資訊")
|
|||
|
print("=" * 50)
|
|||
|
|
|||
|
cache_dir = get_cache_dir()
|
|||
|
print(f"Cache 目錄: {cache_dir}")
|
|||
|
|
|||
|
if cache_dir.exists():
|
|||
|
cache_size = get_cache_size(cache_dir)
|
|||
|
print(f"Cache 大小: {format_size(cache_size)}")
|
|||
|
|
|||
|
# 顯示子目錄大小
|
|||
|
subdirs = []
|
|||
|
for subdir in cache_dir.iterdir():
|
|||
|
if subdir.is_dir():
|
|||
|
subdir_size = get_cache_size(subdir)
|
|||
|
subdirs.append((subdir.name, subdir_size))
|
|||
|
|
|||
|
if subdirs:
|
|||
|
print("\n📁 子目錄大小:")
|
|||
|
subdirs.sort(key=lambda x: x[1], reverse=True)
|
|||
|
for name, size in subdirs[:10]: # 顯示前10大
|
|||
|
print(f" {name}: {format_size(size)}")
|
|||
|
else:
|
|||
|
print("Cache 目錄不存在")
|
|||
|
|
|||
|
def clean_cache_selective(cache_dir, dry_run=False):
|
|||
|
"""選擇性清理 cache,跳過正在使用的檔案"""
|
|||
|
cleaned_count = 0
|
|||
|
skipped_count = 0
|
|||
|
total_saved = 0
|
|||
|
|
|||
|
print(f"🔍 掃描 cache 目錄: {cache_dir}")
|
|||
|
|
|||
|
# 遍歷 cache 目錄
|
|||
|
for root, dirs, files in os.walk(cache_dir):
|
|||
|
# 跳過一些可能正在使用的目錄
|
|||
|
if any(skip_dir in root for skip_dir in ['Scripts', 'Lib', 'pyvenv.cfg']):
|
|||
|
continue
|
|||
|
|
|||
|
for file in files:
|
|||
|
file_path = Path(root) / file
|
|||
|
try:
|
|||
|
if dry_run:
|
|||
|
file_size = file_path.stat().st_size
|
|||
|
total_saved += file_size
|
|||
|
cleaned_count += 1
|
|||
|
if cleaned_count <= 10: # 只顯示前10個
|
|||
|
print(f" 將清理: {file_path.relative_to(cache_dir)} ({format_size(file_size)})")
|
|||
|
else:
|
|||
|
file_size = file_path.stat().st_size
|
|||
|
file_path.unlink()
|
|||
|
total_saved += file_size
|
|||
|
cleaned_count += 1
|
|||
|
except (OSError, PermissionError, FileNotFoundError) as e:
|
|||
|
skipped_count += 1
|
|||
|
if not dry_run and skipped_count <= 5: # 只顯示前5個錯誤
|
|||
|
print(f" ⚠️ 跳過: {file_path.name} (正在使用中)")
|
|||
|
|
|||
|
return cleaned_count, skipped_count, total_saved
|
|||
|
|
|||
|
def clean_cache(dry_run=False):
|
|||
|
"""清理 cache"""
|
|||
|
action = "預覽" if dry_run else "執行"
|
|||
|
print(f"🧹 {action} UV Cache 清理")
|
|||
|
print("=" * 50)
|
|||
|
|
|||
|
# 顯示清理前的大小
|
|||
|
cache_dir = get_cache_dir()
|
|||
|
if cache_dir.exists():
|
|||
|
before_size = get_cache_size(cache_dir)
|
|||
|
print(f"清理前大小: {format_size(before_size)}")
|
|||
|
else:
|
|||
|
print("Cache 目錄不存在,無需清理")
|
|||
|
return
|
|||
|
|
|||
|
if dry_run:
|
|||
|
print("\n🔍 將要清理的內容:")
|
|||
|
# 先嘗試 uv cache clean --dry-run
|
|||
|
result = run_uv_command(["cache", "clean", "--dry-run"], check=False)
|
|||
|
if result and result.returncode == 0:
|
|||
|
print(result.stdout)
|
|||
|
else:
|
|||
|
print(" 使用自定義掃描...")
|
|||
|
cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=True)
|
|||
|
print(f"\n📊 預覽結果:")
|
|||
|
print(f" 可清理檔案: {cleaned_count}")
|
|||
|
print(f" 預計節省: {format_size(total_saved)}")
|
|||
|
else:
|
|||
|
print("\n🗑️ 正在清理...")
|
|||
|
|
|||
|
# 先嘗試標準清理
|
|||
|
result = run_uv_command(["cache", "clean"], check=False)
|
|||
|
if result and result.returncode == 0:
|
|||
|
print("✅ 標準 Cache 清理完成")
|
|||
|
else:
|
|||
|
print("⚠️ 標準清理失敗,使用選擇性清理...")
|
|||
|
cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=False)
|
|||
|
|
|||
|
print(f"\n📊 清理結果:")
|
|||
|
print(f" 已清理檔案: {cleaned_count}")
|
|||
|
print(f" 跳過檔案: {skipped_count}")
|
|||
|
print(f" 節省空間: {format_size(total_saved)}")
|
|||
|
|
|||
|
if skipped_count > 0:
|
|||
|
print(f"\n💡 提示: {skipped_count} 個檔案正在使用中,已跳過")
|
|||
|
print(" 建議關閉相關程序後重新執行清理")
|
|||
|
|
|||
|
# 顯示清理後的大小
|
|||
|
if cache_dir.exists():
|
|||
|
after_size = get_cache_size(cache_dir)
|
|||
|
saved_size = before_size - after_size
|
|||
|
print(f"\n📈 總體效果:")
|
|||
|
print(f" 清理前: {format_size(before_size)}")
|
|||
|
print(f" 清理後: {format_size(after_size)}")
|
|||
|
print(f" 實際節省: {format_size(saved_size)}")
|
|||
|
else:
|
|||
|
print(f" 節省空間: {format_size(before_size)}")
|
|||
|
|
|||
|
def force_clean_cache():
|
|||
|
"""強制清理 cache(關閉相關程序後)"""
|
|||
|
print("🔥 強制清理模式")
|
|||
|
print("=" * 50)
|
|||
|
print("⚠️ 警告:此模式會嘗試關閉可能使用 cache 的程序")
|
|||
|
|
|||
|
confirm = input("確定要繼續嗎?(y/N): ")
|
|||
|
if confirm.lower() != 'y':
|
|||
|
print("❌ 已取消")
|
|||
|
return
|
|||
|
|
|||
|
cache_dir = get_cache_dir()
|
|||
|
if not cache_dir.exists():
|
|||
|
print("Cache 目錄不存在")
|
|||
|
return
|
|||
|
|
|||
|
before_size = get_cache_size(cache_dir)
|
|||
|
print(f"清理前大小: {format_size(before_size)}")
|
|||
|
|
|||
|
# 嘗試關閉可能的 uvx 程序
|
|||
|
print("\n🔍 檢查相關程序...")
|
|||
|
try:
|
|||
|
import psutil
|
|||
|
killed_processes = []
|
|||
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
|||
|
try:
|
|||
|
if proc.info['name'] and any(name in proc.info['name'].lower()
|
|||
|
for name in ['uvx', 'uv.exe', 'python.exe']):
|
|||
|
cmdline = ' '.join(proc.info['cmdline'] or [])
|
|||
|
if 'mcp-feedback-enhanced' in cmdline or 'uvx' in cmdline:
|
|||
|
print(f" 終止程序: {proc.info['name']} (PID: {proc.info['pid']})")
|
|||
|
proc.terminate()
|
|||
|
killed_processes.append(proc.info['pid'])
|
|||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|||
|
pass
|
|||
|
|
|||
|
if killed_processes:
|
|||
|
print(f" 已終止 {len(killed_processes)} 個程序")
|
|||
|
import time
|
|||
|
time.sleep(2) # 等待程序完全關閉
|
|||
|
else:
|
|||
|
print(" 未發現相關程序")
|
|||
|
|
|||
|
except ImportError:
|
|||
|
print(" 無法檢查程序(需要 psutil),繼續清理...")
|
|||
|
|
|||
|
# 再次嘗試標準清理
|
|||
|
print("\n🗑️ 執行清理...")
|
|||
|
result = run_uv_command(["cache", "clean"], check=False)
|
|||
|
if result and result.returncode == 0:
|
|||
|
print("✅ 強制清理成功")
|
|||
|
else:
|
|||
|
print("⚠️ 標準清理仍然失敗,使用檔案級清理...")
|
|||
|
cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=False)
|
|||
|
print(f" 清理檔案: {cleaned_count}, 跳過: {skipped_count}")
|
|||
|
|
|||
|
# 顯示結果
|
|||
|
after_size = get_cache_size(cache_dir)
|
|||
|
saved_size = before_size - after_size
|
|||
|
print(f"\n📈 清理結果:")
|
|||
|
print(f" 節省空間: {format_size(saved_size)}")
|
|||
|
|
|||
|
def main():
|
|||
|
parser = argparse.ArgumentParser(description="UV Cache 清理工具")
|
|||
|
group = parser.add_mutually_exclusive_group(required=True)
|
|||
|
group.add_argument("--size", action="store_true", help="顯示 cache 大小資訊")
|
|||
|
group.add_argument("--dry-run", action="store_true", help="預覽清理內容(不實際清理)")
|
|||
|
group.add_argument("--clean", action="store_true", help="執行 cache 清理")
|
|||
|
group.add_argument("--force", action="store_true", help="強制清理(會嘗試關閉相關程序)")
|
|||
|
|
|||
|
args = parser.parse_args()
|
|||
|
|
|||
|
if args.size:
|
|||
|
show_cache_info()
|
|||
|
elif args.dry_run:
|
|||
|
clean_cache(dry_run=True)
|
|||
|
elif args.clean:
|
|||
|
clean_cache(dry_run=False)
|
|||
|
elif args.force:
|
|||
|
force_clean_cache()
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
main()
|