openclaw-cleaner
OpenClaw清理大师 - 项目目录清理与优化工具。提供快照、Diff、检查点、任务进度等能力,AI 可直接调用自动执行。
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install skilldb:computersniper~openclaw-cleanercURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/skilldb%3Acomputersniper~openclaw-cleaner/file -o openclaw-cleaner.mdGit 仓库获取源码
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,