Files
reverse-skills-max/vendor-new/xbsReverseSkill/web-js-env-patcher/scripts/clean_case.js

397 lines
14 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
function parseArgs(argv) {
const args = {
caseDir: null,
dryRun: false,
force: false,
includeProfiles: false,
json: false,
markdown: false,
pruneEmpty: true,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i];
else if (a === '--dry-run') args.dryRun = true;
else if (a === '--force') args.force = true;
else if (a === '--include-profiles') args.includeProfiles = true;
else if (a === '--no-prune-empty') args.pruneEmpty = false;
else if (a === '--json') args.json = true;
else if (a === '--markdown') args.markdown = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!args.json && !args.markdown) args.markdown = true;
return args;
}
function usage() {
return `用法:
node scripts/clean_case.js --case-dir case --dry-run --markdown
node scripts/clean_case.js --case-dir case --force --markdown
node scripts/clean_case.js --case-dir case --force --include-profiles --markdown
node scripts/clean_case.js --case-dir case --force --no-prune-empty --json
说明:清理 case 内测试文件、临时文件、缓存文件、中间产物和空目录。默认不删除疑似登录态 Profile / Cookie / IndexedDB。`;
}
function exists(p) {
try { fs.accessSync(p); return true; } catch { return false; }
}
function stat(p) {
try { return fs.statSync(p); } catch { return null; }
}
function isDangerousDir(p) {
const root = path.parse(p).root;
const normalized = path.resolve(p);
return normalized === root || normalized.length <= root.length + 2;
}
function normalizeSlash(p) {
return String(p).replace(/\\/g, '/');
}
function relPath(caseDir, p) {
const rel = path.relative(caseDir, p) || '.';
return normalizeSlash(rel);
}
function isInside(parent, child) {
const rel = path.relative(parent, child);
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
}
function isFirefoxProfileDir(p) {
const st = stat(p);
if (!st || !st.isDirectory()) return false;
let names = [];
try { names = fs.readdirSync(p).map(name => name.toLowerCase()); } catch { return false; }
const nameSet = new Set(names);
const markerHits = [
'prefs.js',
'cookies.sqlite',
'places.sqlite',
'storage',
'cache2',
'cert9.db',
'key4.db',
'logins.db',
'webappsstore.sqlite',
'parent.lock',
'sessionstore-backups',
].filter(name => nameSet.has(name)).length;
return markerHits >= 2;
}
function isProfilePath(p) {
const lower = normalizeSlash(p).toLowerCase();
if (/(^|\/)(cloak-profile|camoufox-profile|camoufox-user-data|browser-profile|user-data-dir|user-data|firefox-profile|chrome-profile|ruyipage-profile|profile)(\/|$)/.test(lower)
|| /(^|\/)[^/]*profile[^/]*(\/|$)/.test(lower)
|| /(^|\/)(cookies|local storage|indexeddb|session storage)(\/|$)/.test(lower)
|| /\b(cookie|localstorage|sessionstorage|authorization|token)\b/i.test(lower)) return true;
let cur = path.resolve(p);
const st = stat(cur);
if (!st || !st.isDirectory()) cur = path.dirname(cur);
while (true) {
if (isFirefoxProfileDir(cur)) return true;
const parent = path.dirname(cur);
if (parent === cur) break;
cur = parent;
}
return false;
}
function isDisposableDirName(name) {
const n = name.toLowerCase();
return [
'tmp', '.tmp', 'temp', '.temp', 'cache', '.cache',
'__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache',
'browser-temp', 'camoufox-trace', 'camoufox-values', 'failed', '.downloads', '.tmp-downloads',
].includes(n);
}
function isDisposableFileName(name) {
const n = name.toLowerCase();
if (['.ds_store', 'thumbs.db', 'desktop.ini'].includes(n)) return true;
if (/\.(tmp|temp|bak|old|orig|retry|partial|download|crdownload|cache)$/i.test(n)) return true;
if (/^(env-trace\.jsonl|missing-env\.json|node-output\.json|run-output\.json)$/i.test(n)) return true;
if (/^(fingerprint-hook|.*-fingerprint-hook|hook-fingerprint).*\.(js|mjs|cjs)$/i.test(n)) return true;
if (/^(test-|tmp-|temp-|debug-|scratch-).+\.(js|mjs|cjs|json|jsonl|log|txt|md|html|png|jpg|jpeg|webp|har)$/i.test(n)) return true;
if (/(\.test-output|\.debug-output|\.tmp-output)\./i.test(n)) return true;
return false;
}
function listTree(p, out = []) {
if (!exists(p)) return out;
const st = stat(p);
if (!st) return out;
if (st.isDirectory()) {
let names = [];
try { names = fs.readdirSync(p); } catch { names = []; }
for (const name of names) listTree(path.join(p, name), out);
}
out.push(p);
return out;
}
function listDirsDeepFirst(p) {
const out = [];
function visit(dir) {
if (!exists(dir)) return;
let names = [];
try { names = fs.readdirSync(dir); } catch { names = []; }
for (const name of names) {
const child = path.join(dir, name);
const st = stat(child);
if (st && st.isDirectory()) visit(child);
}
out.push(dir);
}
visit(p);
return out.sort((a, b) => b.length - a.length);
}
function hasProfileInside(p) {
return listTree(p).some(isProfilePath);
}
function isProfileProtectedContainer(p, includeProfiles) {
if (includeProfiles) return false;
const st = stat(p);
return !!st && st.isDirectory() && !isProfilePath(p) && hasProfileInside(p);
}
function hasOnlyProfileChildren(dir) {
const st = stat(dir);
if (!st || !st.isDirectory()) return false;
let names = [];
try { names = fs.readdirSync(dir); } catch { return false; }
if (names.length === 0) return false;
return names.every(name => {
const child = path.join(dir, name);
return isProfilePath(child) || hasProfileInside(child);
});
}
function addAction(actions, action, target, reason) {
actions.push({ action, path: target, reason: reason || '' });
}
function removePath(target, dryRun, recursive = true) {
if (dryRun) return;
const st = stat(target);
if (!st) return;
if (st.isDirectory() && recursive) fs.rmSync(target, { recursive: true, force: true });
else if (st.isDirectory()) fs.rmdirSync(target);
else fs.rmSync(target, { force: true });
}
function cleanDisposableDir(caseDir, dir, args, actions) {
if (!exists(dir)) return;
const st = stat(dir);
if (!st || !st.isDirectory()) return;
if (isProfilePath(dir) && !args.includeProfiles) {
addAction(actions, 'skip-profile', dir, '疑似登录态或浏览器 Profile,默认保留');
return;
}
if (isProfileProtectedContainer(dir, args.includeProfiles)) {
addAction(actions, 'keep-profile-container', dir, '目录内包含疑似 Profile,仅清理非敏感子项');
let names = [];
try { names = fs.readdirSync(dir); } catch { names = []; }
for (const name of names) {
const child = path.join(dir, name);
if (isProfilePath(child) || hasProfileInside(child)) {
addAction(actions, 'skip-profile', child, '疑似登录态或浏览器 Profile,默认保留');
continue;
}
addAction(actions, args.dryRun ? 'would-delete' : 'delete', child, '清理临时目录中的非敏感子项');
removePath(child, args.dryRun, true);
}
return;
}
addAction(actions, args.dryRun ? 'would-delete' : 'delete', dir, '清理临时 / 缓存 / 中间产物目录');
removePath(dir, args.dryRun, true);
}
function isTempLikePath(caseDir, p, includeProfiles) {
if (p === caseDir) return false;
if (!includeProfiles && isProfilePath(p)) return false;
const rel = relPath(caseDir, p).toLowerCase();
const base = path.basename(p).toLowerCase();
const tempLike = rel.split('/').some(part => isDisposableDirName(part)) || isDisposableFileName(base);
if (!tempLike) return false;
const st = stat(p);
if (!includeProfiles && st && st.isDirectory() && isProfileProtectedContainer(p, includeProfiles) && hasOnlyProfileChildren(p)) {
return false;
}
return true;
}
function collectRemainingTempLike(caseDir, includeProfiles) {
return listTree(caseDir).filter(p => exists(p) && isTempLikePath(caseDir, p, includeProfiles));
}
function retryRemainingCleanup(caseDir, args, actions) {
if (args.dryRun) return;
for (let round = 0; round < 2; round++) {
const remaining = collectRemainingTempLike(caseDir, args.includeProfiles);
if (!remaining.length) return;
for (const p of remaining.sort((a, b) => b.length - a.length)) {
if (!exists(p) || p === caseDir || !isInside(caseDir, p)) continue;
const st = stat(p);
if (!st) continue;
if (!args.includeProfiles && isProfilePath(p)) continue;
if (!args.includeProfiles && st.isDirectory() && isProfileProtectedContainer(p, args.includeProfiles)) {
cleanDisposableDir(caseDir, p, args, actions);
continue;
}
if (st.isDirectory()) {
addAction(actions, 'delete', p, '二次清理残留临时目录');
removePath(p, false, true);
} else if (isDisposableFileName(path.basename(p)) || relPath(caseDir, p).toLowerCase().split('/').some(part => isDisposableDirName(part))) {
addAction(actions, 'delete', p, '二次清理残留临时文件');
removePath(p, false, false);
}
}
pruneEmptyDirs(caseDir, args, actions);
}
}
function collectDisposableDirs(caseDir) {
const dirs = [];
const direct = [
'tmp', '.tmp', 'temp', '.temp', 'browser-temp', 'cache', '.cache',
'downloads/failed', 'downloads/.tmp', 'downloads/.cache',
'logs/tmp', 'logs/.tmp', 'trace/tmp', 'trace/.tmp',
'ruyi-trace/tmp', 'ruyi-trace/.tmp', 'camoufox-trace', 'camoufox-values',
'camoufox-trace/tmp', 'camoufox-values/tmp', 'screenshots/tmp', 'screenshots/.tmp',
];
for (const rel of direct) dirs.push(path.join(caseDir, rel));
for (const p of listTree(caseDir)) {
const st = stat(p);
if (!st || !st.isDirectory()) continue;
if (p === caseDir) continue;
if (isDisposableDirName(path.basename(p))) dirs.push(p);
}
return Array.from(new Set(dirs.map(p => path.resolve(p)))).sort((a, b) => b.length - a.length);
}
function cleanDisposableFiles(caseDir, args, actions) {
for (const p of listTree(caseDir)) {
if (!exists(p)) continue;
const st = stat(p);
if (!st || !st.isFile()) continue;
if (isProfilePath(p) && !args.includeProfiles) {
addAction(actions, 'skip-sensitive-file', p, '疑似 Cookie / token / 登录态文件,默认保留');
continue;
}
const rel = relPath(caseDir, p).toLowerCase();
const inTempLikeDir = rel.split('/').some(part => isDisposableDirName(part));
const shouldDelete = isDisposableFileName(path.basename(p)) || (st.size === 0 && inTempLikeDir);
if (!shouldDelete) continue;
addAction(actions, args.dryRun ? 'would-delete' : 'delete', p, st.size === 0 ? '清理空临时文件' : '清理临时 / 测试 / 缓存文件');
removePath(p, args.dryRun, false);
}
}
function pruneEmptyDirs(caseDir, args, actions) {
if (!args.pruneEmpty) return;
for (const dir of listDirsDeepFirst(caseDir)) {
if (dir === caseDir) continue;
if (!exists(dir)) continue;
if (isProfilePath(dir) && !args.includeProfiles) continue;
let names = [];
try { names = fs.readdirSync(dir); } catch { continue; }
if (names.length !== 0) continue;
addAction(actions, args.dryRun ? 'would-remove-empty-dir' : 'remove-empty-dir', dir, '清理空目录');
removePath(dir, args.dryRun, false);
}
}
function cleanup(args) {
if (!args.caseDir) throw new Error('必须提供 --case-dir');
const caseDir = path.resolve(args.caseDir);
if (!exists(caseDir)) throw new Error(`case 目录不存在:${caseDir}`);
const caseStat = stat(caseDir);
if (!caseStat || !caseStat.isDirectory()) throw new Error(`case 路径不是目录:${caseDir}`);
if (isDangerousDir(caseDir)) throw new Error(`拒绝清理危险目录:${caseDir}`);
if (!args.dryRun && !args.force) throw new Error('未提供 --force,拒绝删除;请先使用 --dry-run 预览');
const actions = [];
const disposableDirs = collectDisposableDirs(caseDir);
for (const dir of disposableDirs) {
if (!exists(dir)) continue;
if (dir === caseDir || !isInside(caseDir, dir)) continue;
cleanDisposableDir(caseDir, dir, args, actions);
}
cleanDisposableFiles(caseDir, args, actions);
pruneEmptyDirs(caseDir, args, actions);
retryRemainingCleanup(caseDir, args, actions);
const remainingTempLike = collectRemainingTempLike(caseDir, args.includeProfiles);
return {
caseDir,
dryRun: args.dryRun,
includeProfiles: args.includeProfiles,
pruneEmpty: args.pruneEmpty,
actions,
remainingTempLike,
clean: remainingTempLike.length === 0,
};
}
function renderMarkdown(result) {
const label = {
'skip-profile': '跳过 Profile',
'skip-sensitive-file': '跳过敏感文件',
'keep-profile-container': '保留 Profile 容器目录',
'would-delete': '将删除',
'delete': '已删除',
'would-remove-empty-dir': '将删除空目录',
'remove-empty-dir': '已删除空目录',
};
const lines = ['# 清理结果', '', `case 目录:${result.caseDir}`, `dry-run:${result.dryRun ? '是' : '否'}`, `是否包含 Profile:${result.includeProfiles ? '是' : '否'}`, `是否清理空目录:${result.pruneEmpty ? '是' : '否'}`, ''];
if (result.actions.length) {
lines.push('## 操作列表');
for (const a of result.actions) {
lines.push(`- ${label[a.action] || a.action}:${a.path}${a.reason ? `(${a.reason})` : ''}`);
}
} else {
lines.push('## 操作列表', '- 没有需要清理的内容');
}
lines.push('', '## 清理后检查');
lines.push(`- 是否仍存在普通临时 / 缓存 / 中间产物:${result.clean ? '否' : '是'}`);
if (result.remainingTempLike.length) {
for (const p of result.remainingTempLike) lines.push(` - ${p}`);
}
return lines.join('\n') + '\n';
}
try {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); process.exit(0); }
const result = cleanup(args);
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(renderMarkdown(result));
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}