feat: add code queue services and baidu netdisk

This commit is contained in:
Codex
2026-05-13 00:59:36 +00:00
parent ae462ed9ef
commit 6a04144d3f
63 changed files with 10983 additions and 1101 deletions
+6
View File
@@ -68,6 +68,12 @@ export interface DockerContainerSummary {
runningFor: string;
size: string;
networks: string;
restartPolicy?: string;
restartPolicyMaximumRetryCount?: number;
pidMode?: string;
composeProject?: string;
composeService?: string;
labels?: Record<string, string>;
}
export interface DockerImageSummary {
+165
View File
@@ -0,0 +1,165 @@
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
import { basename, dirname, join } from "node:path";
export const DEFAULT_LOG_RETENTION_BYTES = 1024 * 1024 * 1024;
export interface HourlyJsonlWriter {
appendLine(line: string, at?: Date): void;
appendJson(value: unknown, at?: Date): void;
currentPath(at?: Date): string;
prune(at?: Date): void;
}
interface HourlyJsonlWriterOptions {
baseLogFile: string;
service: string;
maxBytes?: number;
}
interface FileEntry {
path: string;
size: number;
mtimeMs: number;
}
function pad(value: number): string {
return String(value).padStart(2, "0");
}
function localParts(date: Date): { day: string; hour: string } {
return {
day: `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`,
hour: pad(date.getHours()),
};
}
function safeServiceName(service: string): string {
return service.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "service";
}
function parseBase(baseLogFile: string, service: string): { rootDir: string; prefix: string; suffix: string } {
const suffix = `_${service}.jsonl`;
const dir = dirname(baseLogFile);
const dirName = basename(dir);
const rootDir = /^\d{8}$/u.test(dirName) ? dirname(dir) : dir;
const fileName = basename(baseLogFile);
const stem = fileName.replace(/\.jsonl$/u, "");
const ownPrefix = fileName.endsWith(suffix) ? fileName.slice(0, -suffix.length) : "";
const startStampPrefix = stem.match(/^(\d{8}_\d{6})(?:_\d{8}_\d{2})?_/u)?.[1] ?? "";
const genericPrefix = stem.includes("_") ? stem.slice(0, stem.lastIndexOf("_")) : stem;
const prefix = ownPrefix || startStampPrefix || genericPrefix;
return { rootDir, prefix: prefix || "unidesk", suffix };
}
function collectFiles(root: string, suffix: string): FileEntry[] {
if (!existsSync(root)) return [];
const result: FileEntry[] = [];
const scan = (dir: string): void => {
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
scan(path);
continue;
}
if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
try {
const stat = statSync(path);
result.push({ path, size: stat.size, mtimeMs: stat.mtimeMs });
} catch {
// Ignore files that disappear while retention is scanning.
}
}
};
scan(root);
return result;
}
export function logRetentionBytesFromEnv(name: string, fallback = DEFAULT_LOG_RETENTION_BYTES): number {
const raw = process.env[name]?.trim();
if (!raw) return fallback;
const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|k|kb|kib|m|mb|mib|g|gb|gib)?$/iu);
if (!match) return fallback;
const value = Number(match[1]);
const unit = (match[2] ?? "b").toLowerCase();
const multiplier =
unit === "g" || unit === "gb" || unit === "gib" ? 1024 * 1024 * 1024
: unit === "m" || unit === "mb" || unit === "mib" ? 1024 * 1024
: unit === "k" || unit === "kb" || unit === "kib" ? 1024
: 1;
const bytes = value * multiplier;
return Number.isFinite(bytes) && bytes > 0 ? Math.floor(bytes) : fallback;
}
export function logRetentionEnvNameForService(service: string): string {
const normalized = safeServiceName(service).toUpperCase().replace(/[^A-Z0-9]+/g, "_");
return `UNIDESK_${normalized}_LOG_MAX_BYTES`;
}
export function logRetentionBytesForService(service: string, extraEnvNames: string[] = [], fallback = DEFAULT_LOG_RETENTION_BYTES): number {
const globalMaxBytes = logRetentionBytesFromEnv("UNIDESK_LOG_RETENTION_BYTES", fallback);
for (const name of [logRetentionEnvNameForService(service), ...extraEnvNames]) {
if (process.env[name]?.trim()) return logRetentionBytesFromEnv(name, globalMaxBytes);
}
return globalMaxBytes;
}
export function createHourlyJsonlWriter(options: HourlyJsonlWriterOptions): HourlyJsonlWriter {
const service = safeServiceName(options.service);
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_LOG_RETENTION_BYTES));
const { rootDir, prefix, suffix } = parseBase(options.baseLogFile, service);
let lastPruneAt = 0;
const currentPath = (at = new Date()): string => {
const parts = localParts(at);
return join(rootDir, parts.day, `${prefix}_${parts.day}_${parts.hour}${suffix}`);
};
const prune = (at = new Date()): void => {
const activePath = currentPath(at);
const files = collectFiles(rootDir, suffix).sort((left, right) => {
if (left.path === activePath) return 1;
if (right.path === activePath) return -1;
return left.path.localeCompare(right.path) || left.mtimeMs - right.mtimeMs;
});
let total = files.reduce((sum, file) => sum + file.size, 0);
for (const file of files) {
if (total <= maxBytes) break;
if (file.path === activePath) continue;
try {
unlinkSync(file.path);
total -= file.size;
} catch {
// Best-effort retention must never break service logging.
}
}
};
const maybePrune = (at: Date): void => {
const now = Date.now();
if (now - lastPruneAt < 60_000) return;
lastPruneAt = now;
prune(at);
};
const appendLine = (line: string, at = new Date()): void => {
const path = currentPath(at);
mkdirSync(dirname(path), { recursive: true });
appendFileSync(path, line.endsWith("\n") ? line : `${line}\n`, "utf8");
maybePrune(at);
};
return {
appendLine,
appendJson(value: unknown, at = new Date()): void {
appendLine(JSON.stringify(value), at);
},
currentPath,
prune,
};
}