- Fix hermes invocation: 'hermes -q' → 'hermes chat -q' with proper flags
- Replace fragile string.includes('PASS') with llmExtract judge
(previous false positive: matched '--pass-session-id' in usage text)
小橘 🍊(NEKO Team)
132 lines
3.2 KiB
JavaScript
132 lines
3.2 KiB
JavaScript
import { execSync } from "node:child_process";
|
|
import { diskUsageMounts } from "./schema.ts";
|
|
|
|
const DF_CMD =
|
|
"df -B1 --output=source,target,fstype,size,used,avail,pcent";
|
|
|
|
/** fstype-based exclusions to avoid pseudo / volatile filesystem noise */
|
|
const EXCLUDED_FSTYPES = new Set([
|
|
"tmpfs",
|
|
"devtmpfs",
|
|
"proc",
|
|
"sysfs",
|
|
"cgroup2",
|
|
"cgroup",
|
|
]);
|
|
|
|
function round2(n) {
|
|
return Math.round(n * 100) / 100;
|
|
}
|
|
|
|
function parseUIntField(s) {
|
|
if (s === "-") return null;
|
|
const n = Number.parseInt(String(s), 10);
|
|
if (!Number.isFinite(n) || n < 0) return null;
|
|
return n;
|
|
}
|
|
|
|
function parsePcent(tok) {
|
|
if (!tok || typeof tok !== "string") return null;
|
|
const t = tok.trim();
|
|
if (!/^[\d.]+%$/.test(t)) return null;
|
|
const raw = Number.parseFloat(t.replace("%", ""));
|
|
return Number.isFinite(raw) ? round2(raw) : null;
|
|
}
|
|
|
|
/**
|
|
* Parse one `df --output=source,target,fstype,size,used,avail,pcent` data line.
|
|
* Mount may contain spaces; last five logical columns are fixed.
|
|
*/
|
|
function parseDfLine(line) {
|
|
const parts = line.trim().split(/\s+/);
|
|
if (parts.length < 7) return null;
|
|
|
|
const pcentTok = parts[parts.length - 1];
|
|
const availBytes = parseUIntField(parts[parts.length - 2]);
|
|
const usedBytes = parseUIntField(parts[parts.length - 3]);
|
|
const totalBytes = parseUIntField(parts[parts.length - 4]);
|
|
const fstype = parts[parts.length - 5];
|
|
const device = parts[0];
|
|
const mount = parts.slice(1, parts.length - 5).join(" ");
|
|
|
|
if (!device || !mount || !fstype) return null;
|
|
if (totalBytes === null || usedBytes === null || availBytes === null)
|
|
return null;
|
|
|
|
const pcentFromDf = parsePcent(pcentTok);
|
|
const computed =
|
|
totalBytes > 0 ? round2((usedBytes / totalBytes) * 100) : 0;
|
|
let usedPercent = computed;
|
|
if (pcentFromDf !== null) {
|
|
const diff = Math.abs(computed - pcentFromDf);
|
|
usedPercent = diff > 1 ? computed : pcentFromDf;
|
|
}
|
|
|
|
return {
|
|
device,
|
|
mount,
|
|
fstype,
|
|
totalBytes,
|
|
usedBytes,
|
|
availBytes,
|
|
usedPercent,
|
|
};
|
|
}
|
|
|
|
function parseDfOutput(text) {
|
|
const rows = [];
|
|
const lines = text.split("\n");
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
if (/^Filesystem\s+/.test(trimmed) || trimmed.startsWith("Filesystem"))
|
|
continue;
|
|
const row = parseDfLine(line);
|
|
if (row && !EXCLUDED_FSTYPES.has(row.fstype)) rows.push(row);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
export async function compute(db, _peers) {
|
|
const ts = Date.now();
|
|
let mounts = [];
|
|
|
|
try {
|
|
const out = execSync(DF_CMD, {
|
|
encoding: "utf-8",
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
});
|
|
mounts = parseDfOutput(out);
|
|
} catch {
|
|
mounts = [];
|
|
}
|
|
|
|
if (mounts.length > 0) {
|
|
await db.insert(diskUsageMounts).values(
|
|
mounts.map((m) => ({
|
|
ts,
|
|
device: m.device,
|
|
mount: m.mount,
|
|
fstype: m.fstype,
|
|
totalBytes: m.totalBytes,
|
|
usedBytes: m.usedBytes,
|
|
availBytes: m.availBytes,
|
|
usedPercent: m.usedPercent,
|
|
})),
|
|
);
|
|
}
|
|
|
|
return {
|
|
ts,
|
|
mounts: mounts.map((m) => ({
|
|
device: m.device,
|
|
mount: m.mount,
|
|
fstype: m.fstype,
|
|
totalBytes: m.totalBytes,
|
|
usedBytes: m.usedBytes,
|
|
availBytes: m.availBytes,
|
|
usedPercent: m.usedPercent,
|
|
})),
|
|
};
|
|
}
|