- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次) - .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…), 目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪 - .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/) - .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*) - .gitignore 补:备份件(*.bak-*) - 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
80 lines
4.3 KiB
JavaScript
80 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* session-gc.cjs —— 会话记录保留期回收(档案 14 §H3 / 档案 28)
|
||
* 规则(用户 2026-09-11 定):**每个用户的 sessions 达到阈值才触发**,清理 **超过 N 天** 的会话目录。
|
||
* 结构:<home>/sessions/<workspace-slug>/<session-id>/session.jsonl.zstd
|
||
* 安全:默认 dry-run;--apply 时 `mv` 到 <userRoot>/trash/<日期>-session-gc/(保留 30 天)。
|
||
* 说明:storages/session_projcache 体积很小(KB 级),本脚本不动它(留作后续细化)。
|
||
*
|
||
* 用法:node session-gc.cjs [--apply] [--threshold 500] [--days 90] [--user-id <uuid>]
|
||
*/
|
||
const { execFileSync } = require('node:child_process')
|
||
const { existsSync, mkdirSync, readdirSync, statSync } = require('node:fs')
|
||
const { join } = 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', 500)
|
||
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
|
||
|
||
const duKB = (p) => { try { return Number(execFileSync('du', ['-sk', p], { encoding: 'utf8' }).split(/\s+/)[0]) * 1024 } catch { return 0 } }
|
||
const fmt = (b) => (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 sessions = join(u.home_dir, 'sessions')
|
||
if (!existsSync(sessions)) { console.log(` ${u.username}: NO_SESSIONS`); continue }
|
||
const total = duKB(sessions)
|
||
const over = total >= THRESHOLD_MB * 1048576
|
||
const stale = []
|
||
for (const slug of readdirSync(sessions)) {
|
||
const slugDir = join(sessions, slug)
|
||
let st; try { st = statSync(slugDir) } catch { continue }
|
||
if (!st.isDirectory()) continue
|
||
for (const sid of readdirSync(slugDir)) {
|
||
const sd = join(slugDir, sid)
|
||
let sst; try { sst = statSync(sd) } catch { continue }
|
||
if (!sst.isDirectory()) continue
|
||
const f = join(sd, 'session.jsonl.zstd')
|
||
let mtime = sst.mtimeMs
|
||
try { if (existsSync(f)) mtime = statSync(f).mtimeMs } catch {}
|
||
if (mtime < CUTOFF) stale.push({ p: sd, size: duKB(sd), mtime: new Date(mtime) })
|
||
}
|
||
}
|
||
const staleB = stale.reduce((a, c) => a + c.size, 0)
|
||
console.log(` ${u.username}: sessions=${fmt(total)} (${over ? '≥' : '<'} 阈值 ${THRESHOLD_MB}MB) | 超 ${DAYS} 天会话=${stale.length} 个 / ${fmt(staleB)}`)
|
||
for (const c of stale) console.log(` ${c.p.split('/').slice(-2).join('/')} ${fmt(c.size)} (${c.mtime.toISOString().slice(0, 10)})`)
|
||
if (APPLY && over && stale.length > 0) {
|
||
const trash = join(u.home_dir, '..', 'trash', `${STAMP}-session-gc`)
|
||
mkdirSync(trash, { recursive: true })
|
||
for (const c of stale) {
|
||
const dest = join(trash, c.p.split('/').slice(-2).join('__'))
|
||
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]}`) }
|
||
}
|
||
// 同步回收投影缓存:storages/session_projcache/sessions/<session-id>.json(按 id 精确匹配)
|
||
const pcDir = join(u.home_dir, 'storages', 'session_projcache', 'sessions')
|
||
let pcMoved = 0
|
||
if (existsSync(pcDir)) {
|
||
for (const c of stale) {
|
||
const sid = c.p.split('/').pop()
|
||
const pc = join(pcDir, `${sid}.json`)
|
||
if (existsSync(pc)) {
|
||
try { execFileSync('mv', [pc, join(trash, `projcache__${sid}.json`)]); pcMoved += 1 } catch {}
|
||
}
|
||
}
|
||
}
|
||
console.log(` → 已移 ${stale.length} 个会话 / ${fmt(staleB)}${pcMoved > 0 ? `(+ ${pcMoved} 个投影缓存)` : ''} → trash/${STAMP}-session-gc(保留 30 天)`)
|
||
} else if (APPLY && !over) console.log(' (未超阈值,跳过)')
|
||
}
|
||
db.close()
|
||
console.log(APPLY ? 'done(已按阈值执行)' : 'done(dry-run,未改动)')
|