把散落在代码里的真实部署值统一收进 config/,代码改为引用配置, 使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。 新增 config/:platform.env.example(模板)· load.sh(shell 加载器)· index.cjs(node 加载器)· README.md(键一览与优先级)。 真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。 TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用): platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。 config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由 DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径, src/** 注释中性化 116 行/53 文件。 scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径 一律改从配置取;web/wake.html 的注册域白名单改为运行时从 location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737 保留值,并把「内置种子必须为空」固化为回归断言。 取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有); 全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归 (/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
81 lines
4.3 KiB
JavaScript
81 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
||
const cfg = require('../config/index.cjs')
|
||
/**
|
||
* 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('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(cfg.dbFile(), { 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,未改动)')
|