import path from 'node:path' import { PROJECT_ROOT } from '../../config/runtime.js' import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js' const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json') const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health' type JsonObject = Record export function getScheduledJobsFilePath() { return SCHEDULED_JOBS_FILE_PATH } export function getScheduledJobsConfig() { return loadScheduledJobsConfigFromFile() } export function saveScheduledJobsConfig(rawValue: unknown) { return writeJsonFile(SCHEDULED_JOBS_FILE_PATH, rawValue, normalizeScheduledJobsConfig) } export function getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobsConfig()) { return (Array.isArray(config.jobs) ? config.jobs : []) .find((item) => String(item.id || '').trim() === CLOUDTENTACLES_HEALTH_JOB_ID) || createDefaultCloudtentaclesHealthJob() } function loadScheduledJobsConfigFromFile() { return readJsonFile( SCHEDULED_JOBS_FILE_PATH, createDefaultScheduledJobsConfig, normalizeScheduledJobsConfig, ) } export function normalizeScheduledJobsConfig(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} const rawJobs = Array.isArray(source.jobs) ? source.jobs : [] const jobs = rawJobs .map((item) => normalizeScheduledJob(item)) .filter((item): item is ReturnType => Boolean(item)) const hasCloudtentaclesHealth = jobs.some((item) => item.id === CLOUDTENTACLES_HEALTH_JOB_ID) if (!hasCloudtentaclesHealth) { jobs.push(createDefaultCloudtentaclesHealthJob()) } return { enabled: typeof source.enabled === 'boolean' ? source.enabled : true, jobs, } } function normalizeScheduledJob(rawValue: unknown) { if (!isPlainObject(rawValue)) { return null } const type = String(rawValue.type || '').trim() if (type !== 'cloudtentacles_health') { return null } return normalizeCloudtentaclesHealthJob(rawValue) } function normalizeCloudtentaclesHealthJob(rawValue: JsonObject) { const config = isPlainObject(rawValue.config) ? rawValue.config : {} const defaultAssetThreshold = normalizeRangeInteger(config.assetThreshold, 500, 0, 999999) return { id: CLOUDTENTACLES_HEALTH_JOB_ID, type: 'cloudtentacles_health', enabled: rawValue.enabled === true, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 86400), cooldownSeconds: normalizeRangeInteger(rawValue.cooldownSeconds, 1800, 60, 86400), config: { assetThreshold: defaultAssetThreshold, accounts: normalizeCloudtentaclesHealthAccounts(config, defaultAssetThreshold), }, } } function normalizeCloudtentaclesHealthAccounts(config: JsonObject, defaultAssetThreshold: number) { const rawAccounts = Array.isArray(config.accounts) ? config.accounts : Array.isArray(config.sourceKeys) ? config.sourceKeys.map((sourceKey) => ({ sourceKey })) : [] const accounts = rawAccounts .map((item) => normalizeCloudtentaclesHealthAccount(item, defaultAssetThreshold)) .filter(Boolean) as ReturnType[] if (accounts.length === 0) { accounts.push(normalizeCloudtentaclesHealthAccount({ sourceKey: config.sourceKey || 'default', assetThreshold: defaultAssetThreshold, }, defaultAssetThreshold)) } const seen = new Set() return accounts.filter((item) => { const sourceKey = String(item?.sourceKey || '').trim() if (!sourceKey || seen.has(sourceKey)) { return false } seen.add(sourceKey) return true }) } function normalizeCloudtentaclesHealthAccount(rawValue: unknown, defaultAssetThreshold: number) { const source = isPlainObject(rawValue) ? rawValue : {} const sourceKey = String(source.sourceKey || source.key || '').trim() || 'default' return { sourceKey, label: String(source.label || '').trim(), enabled: source.enabled !== false, assetThreshold: normalizeRangeInteger( source.assetThreshold ?? source.threshold, defaultAssetThreshold, 0, 999999, ), } } function createDefaultScheduledJobsConfig() { return { enabled: true, jobs: [ createDefaultCloudtentaclesHealthJob(), ], } } function createDefaultCloudtentaclesHealthJob() { return { id: CLOUDTENTACLES_HEALTH_JOB_ID, type: 'cloudtentacles_health', enabled: false, intervalSeconds: 300, cooldownSeconds: 1800, config: { assetThreshold: 500, accounts: [ { sourceKey: 'default', label: '默认账号', enabled: true, assetThreshold: 500, }, ], }, } } function normalizeRangeInteger(value: unknown, fallback: number, min: number, max: number) { const parsed = Number(value) if (!Number.isInteger(parsed)) { return fallback } return Math.min(max, Math.max(min, parsed)) } function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' }