Files
dsh_ai1net_server/归档/poc-dsh-local/ws-cleanup.cjs
T
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

94 lines
5.2 KiB
JavaScript
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
/**
* ws-cleanup.cjs —— 用户工作区(ws)定期清理 + 磁盘画像(档案 28)
*
* 分三档(**判定依据只有"路径/文件名模式 + 年龄"**,因为无法可靠区分"AI 写的"):
* T1 明确垃圾/平台产物 → 直接清(白名单精确匹配)
* T2 临时脚本(一次性、无复用价值)→ 超过 N 天未修改才清(默认 90 天)
* T3 其余一切(文档/表格/图片/视频/数据/目录)→ **永不自动删**,只统计
*
* 安全:默认 dry-run;--apply 时一律 `mv` 到 <userRoot>/trash/<日期>-ws-cleanup/(保留 30 天可恢复);
* 仅当该用户 ws ≥ --threshold MB 才动手(默认 2048 MB)。
*
* 用法:
* node ws-cleanup.cjs # dry-run + 画像
* node ws-cleanup.cjs --apply # 执行(含阈值判断)
* node ws-cleanup.cjs --apply --threshold 1024 --days 60
* node ws-cleanup.cjs --user-id <uuid>
*/
const { execFileSync } = require('node:child_process')
const { existsSync, mkdirSync, readdirSync, statSync } = require('node:fs')
const { join, extname, basename } = require('node:path')
const Database = require('/opt/dshs/node_modules/better-sqlite3')
const argv = process.argv.slice(2)
const APPLY = argv.includes('--apply')
const num = (flag, dflt) => { const i = argv.indexOf(flag); const v = i >= 0 ? Number(argv[i + 1]) : NaN; return Number.isFinite(v) ? v : dflt }
const THRESHOLD_MB = num('--threshold', 2048)
const DAYS = num('--days', 90)
const idx = argv.indexOf('--user-id')
const ONLY = idx >= 0 ? argv[idx + 1] : ''
const STAMP = new Date().toISOString().slice(0, 10)
const CUTOFF = Date.now() - DAYS * 86400_000
// T1:平台产物/明确垃圾(精确名或后缀);T2:一次性脚本(仅"顶层脚本文件"才算,避免误伤项目目录里的源码)
const T1_DIRS = ['.local', '.cache', '.poc-backup', 'poc', '__pycache__', '.pytest_cache', 'node_modules', '.ipynb_checkpoints']
const T1_SUFFIX = ['.tgz', '.tmp', '.log', '.bak', '.part', '.crdownload']
const T2_EXT = new Set(['.py', '.js', '.mjs', '.cjs', '.sh', '.bash', '.ps1', '.psm1', '.bat', '.cmd', '.ts', '.ipynb', '.sql'])
const duKB = (p) => { try { return Number(execFileSync('du', ['-sk', p], { encoding: 'utf8' }).split(/\s+/)[0]) * 1024 } catch { return 0 } }
const fmt = (b) => (b >= 1073741824 ? (b / 1073741824).toFixed(2) + ' GB' : b >= 1048576 ? (b / 1048576).toFixed(1) + ' MB' : (b / 1024).toFixed(0) + ' KB')
const db = new Database('/var/lib/dshs/dshs.db', { readonly: true })
const users = db.prepare('SELECT id, username, uid, home_dir FROM users').all().filter((u) => ONLY === '' || u.id === ONLY)
for (const u of users) {
const ws = join(u.home_dir, '..', 'ws')
if (!existsSync(ws)) { console.log(` ${u.username}: NO_WS`); continue }
const wsBytes = duKB(ws)
const t1 = [], t2 = [] // T2 可被 ws/.keep 豁免
let otherBytes = 0, otherCount = 0
for (const entry of readdirSync(ws)) {
const p = join(ws, entry)
let st
try { st = statSync(p) } catch { continue }
if (st.isDirectory()) {
if (T1_DIRS.includes(entry)) { t1.push({ p, size: duKB(p) }); continue }
otherBytes += duKB(p); otherCount += 1 // 目录一律算用户资产(T3)
continue
}
const ext = extname(entry).toLowerCase()
if (T1_SUFFIX.includes(ext)) { t1.push({ p, size: st.size }); continue }
if (T2_EXT.has(ext) && st.mtimeMs < CUTOFF) { t2.push({ p, size: st.size, mtime: st.mtime }); continue }
otherBytes += st.size; otherCount += 1
}
// .keep 豁免(档案 28):ws 顶层放一个 .keep 文件 → 该用户**跳过 T2**(一次性脚本不清理),T1 仍清
const keepAll = existsSync(join(ws, '.keep'))
if (keepAll && t2.length > 0) {
console.log(` (.keep 已存在 → T2 保留 ${t2.length} 项,不清理)`)
t2.length = 0
}
const t1B = t1.reduce((a, c) => a + c.size, 0), t2B = t2.reduce((a, c) => a + c.size, 0)
const over = wsBytes >= THRESHOLD_MB * 1048576
console.log(` ${u.username}: ws=${fmt(wsBytes)} (${over ? '≥' : '<'} 阈值 ${THRESHOLD_MB}MB) | T1=${t1.length}项/${fmt(t1B)} | T2=${t2.length}项(>${DAYS}天)/${fmt(t2B)} | 资产=${otherCount}项/${fmt(otherBytes)}`)
for (const c of t1) console.log(` T1 ${basename(c.p)} ${fmt(c.size)}`)
for (const c of t2) console.log(` T2 ${basename(c.p)} ${fmt(c.size)} (mtime ${c.mtime.toISOString().slice(0, 10)})`)
if (APPLY && over && (t1.length + t2.length) > 0) {
const trash = join(u.home_dir, '..', 'trash', `${STAMP}-ws-cleanup`)
mkdirSync(trash, { recursive: true })
for (const c of [...t1, ...t2]) {
const dest = join(trash, c.p.slice(ws.length + 1).replace(/\//g, '__'))
try { execFileSync('mv', [c.p, dest]); execFileSync('chown', ['-R', `${u.uid}:${u.uid}`, dest]) }
catch (e) { console.log(` ! 失败 ${c.p}: ${String(e.message).split('\n')[0]}`) }
}
console.log(` → 已清 ${t1.length + t2.length} 项 / ${fmt(t1B + t2B)} → trash/${STAMP}-ws-cleanup(保留 30 天)`)
} else if (APPLY && !over) {
console.log(` (未超阈值,跳过清理)`)
}
}
db.close()
console.log(APPLY ? 'done(已按阈值执行)' : 'done(dry-run,未改动)')