openclaw-cleaner

SkillDB 作者 computersniper v2.1.2

OpenClaw清理大师 - 项目目录清理与优化工具。提供快照、Diff、检查点、任务进度等能力,AI 可直接调用自动执行。

源码 ↗

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install skilldb:computersniper~openclaw-cleaner
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/skilldb%3Acomputersniper~openclaw-cleaner/file -o openclaw-cleaner.md
Git 仓库获取源码
git clone https://github.com/openclaw/skills/commit/3a313c5abc6ed7c41346e08041e7960f12931df3
# 🧹 OpenClaw Cleaner (清理大师)

> AI 可直接调用的项目清理与优化工具

本技能提供一系列自动化能力,AI 可在执行任务时直接调用,无需用户确认。

---

## 能力总览

| 能力 | 说明 | 何时调用 |
|------|------|----------|
| **快照** | 记录文件状态 | 执行修改前 |
| **Diff** | 对比变更差异 | 修改完成后展示给用户 |
| **Checkpoint** | 保存关键状态 | 重要操作前 |
| **Task Progress** | 追踪长时间任务 | 批量处理时 |
| **Health Check** | 评估目录健康度 | 用户询问或定期 |
| **Skill Match** | 推荐相关 Skills | 用户描述任务时 |
| **Optimize** | 优化文件减少 Token | 空间不足或定期 |

---

## 使用示例

### 场景:删除一个 Skill

```javascript
// 1. 先创建快照(自动,也可手动)
const snap = await snapshot.create('before-remove-old-skill');

// 2. 执行删除
await skills.remove('unused-skill');

// 3. 对比变更,展示给用户
const diff = await snapshot.compare(snap.name, 'latest');
console.log(snapshot.generateReport(diff));
// 用户看到: "我删除了 xxx,新增了 yyy"
```

### 场景:批量处理文件

```javascript
// 1. 开始任务
const taskInfo = await task.start('process-files', fileList);

// 2. 处理每个文件
for (const file of files) {
  try {
    await processFile(file);
    await task.markCompleted(taskInfo.id, index);
  } catch (e) {
    await task.markFailed(taskInfo.id, index, e.message);
  }
}

// 3. 用户可随时查看进度
const status = await task.getStatus(taskInfo.id);
```

---

## 返回格式

```javascript
// snapshot.compare()
{
  added: [{ path: string, size: number, content?: string }],
  removed: [{ path: string, size: number }],
  modified: [{ path: string, oldSize: number, newSize: number, changes: [] }]
}

// health.check()
{
  score: number,           // 0-100
  totalFiles: number,
  totalDirs: number,
  totalSize: number,
  warnings: string[]
}
```

---

## 版本

**v2.1.2** - 整合为单文件版本

---

<!-- 代码实现开始 -->
```javascript
// ============================================================================
// OpenClaw Cleaner - 完整实现代码 (2.1.2)
// ============================================================================

import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';

// 获取工作空间路径
function getWorkspacePath() { return process.cwd(); }

// 静默日志
const silentLogger = { info: () => {}, debug: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {} };

// ============================================================================
// VisualDiffService - 快照和 Diff 服务
// ============================================================================
class VisualDiffService {
  constructor(workspacePath, options = {}) {
    this.workspacePath = workspacePath;
    this.backupDir = options.backupDir || '.cleaner-backups';
    this.logger = options.logger;
  }
  getBackupPath(name = 'default') { return path.join(this.workspacePath, this.backupDir, name); }

  async scanDirectory(dirPath, basePath = dirPath) {
    const files = {};
    async function walk(dir) {
      const entries = await fs.readdir(dir, { withFileTypes: true });
      for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        const relativePath = path.relative(basePath, fullPath);
        if (entry.isDirectory()) {
          if (['node_modules', '.git', '.cleaner-backups', '__pycache__'].includes(entry.name)) continue;
          await walk(fullPath);
        } else if (entry.isFile()) {
          try {
            const stat = await fs.stat(fullPath);
            const content = await fs.readFile(fullPath, 'utf-8');
            const hash = crypto.createHash('md5').update(content).digest('hex');
            files[relativePath] = { path: relativePath, size: stat.size, modified: stat.mtime.toISOString(), hash, content };
          } catch {}
        }
      }
    }
    await walk(dirPath);
    return files;
  }

  async createSnapshot(name = 'manual') {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const snapshotName = `snapshot_${name}_${timestamp}`;
    const snapshotPath = path.join(this.getBackupPath(), 'snapshots', snapshotName);
    await fs.mkdir(snapshotPath, { recursive: true });
    const files = await this.scanDirectory(this.workspacePath, this.workspacePath);
    await fs.writeFile(path.join(snapshotPath, 'files.json'), JSON.stringify(files, null, 2));
    await fs.writeFile(path.join(snapshotPath, '.meta.json'), JSON.stringify({ name: snapshotName, createdAt: new Date().toISOString(), fileCount: Object.keys(files).length }, null, 2));
    return { name: snapshotName, path: snapshotPath, fileCount: Object.keys(files).length };
  }

  async compare(snapshotA, snapshotB) {
    const pathA = snapshotA.includes('/') ? snapshotA : path.join(this.getBackupPath(), 'snapshots', snapshotA);
    const pathB = snapshotB.includes('/') ? snapshotB : path.join(this.getBackupPath(), 'snapshots', snapshotB);
    let filesA, filesB;
    try { filesA = JSON.parse(await fs.readFile(path.join(pathA, 'files.json'), 'utf-8')); } catch { throw new Error(`Snapshot A not found: ${snapshotA}`); }
    try { filesB = JSON.parse(await fs.readFile(path.join(pathB, 'files.json'), 'utf-8')); } catch { throw new Error(`Snapshot B not found: ${snapshotB}`); }
    const diff = { added: [], removed: [], modified: [], unchanged: [] };
    const pathsA = new Set(Object.keys(filesA)), pathsB = new Set(Object.keys(filesB));
    for (const filePath of pathsB) { if (!pathsA.has(filePath)) diff.added.push({ path: filePath, size: filesB[filePath].size, content: filesB[filePath].content }); }
    for (const filePath of pathsA) { if (!pathsB.has(filePath)) diff.removed.push({ path: filePath, size: filesA[filePath].size, content: filesA[filePath].content }); }
    for (const filePath of pathsA) { if (pathsB.has(filePath) && filesA[filePath].hash !== filesB[filePath].hash) diff.modified.push({ path: filePath, oldSize: filesA[filePath].size, newSize: filesB[filePath].size, oldContent: filesA[filePath].content, newContent: filesB[filePath].content }); }
    return diff;
  }

  generateReport(diff) {
    const lines = [];
    lines.push('═'.repeat(60)); lines.push('📊 可视化 Diff 报告'); lines.push('═'.repeat(60));
    lines.push(`\n📈 统计: 🟢 新增 ${diff.added.length} | 🔴 删除 ${diff.removed.length} | 🟡 修改 ${diff.modified.length}\n`);
    if (diff.added.length > 0) { lines.push('🟢 新增文件:'); for (const f of diff.added) lines.push(`  + ${f.path} (${f.size}B)`); lines.push(''); }
    if (diff.removed.length > 0) { lines.push('🔴 删除文件:'); for (const f of diff.removed) lines.push(`  - ${f.path} (${f.size}B)`); lines.push(''); }
    if (diff.modified.length > 0) { lines.push('🟡 修改文件:'); for (const f of diff.modified) lines.push(`  ~ ${f.path} (${f.oldSize}B → ${f.newSize}B)`); lines.push(''); }
    lines.push('═'.repeat(60));
    return lines.join('\n');
  }

  async listSnapshots() {
    const snapshotsPath = path.join(this.getBackupPath(), 'snapshots');
    try {
      const entries = await fs.readdir(snapshotsPath, { withFileTypes: true });
      const snapshots = [];
      for (const entry of entries) {
        if (entry.isDirectory()) {
          try { const meta = JSON.parse(await fs.readFile(path.join(snapshotsPath, entry.name, '.meta.json'), 'utf-8')); snapshots.push({ name: entry.name, createdAt: meta.createdAt, fileCount: meta.fileCount }); }
          catch { snapshots.push({ name: entry.name, createdAt: null }); }
        }
      }
      return snapshots.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
    } catch { return []; }
  }
}

// ============================================================================
// CheckpointService - 检查点服务
// ============================================================================
class CheckpointService {
  constructor(workspacePath, options = {}) { this.workspacePath = workspacePath; this.checkpointDir = options.checkpointDir || '.cleaner-backups/checkpoints'; this.logger = options.logger; }
  getCheckpointPath() { return path.join(this.workspacePath, this.checkpointDir); }

  async create(name, data = {}) {
    const checkpointPath = this.getCheckpointPath();
    await fs.mkdir(checkpointPath, { recursive: true });
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const checkpoint = { name,