264 lines
8.0 KiB
TypeScript
264 lines
8.0 KiB
TypeScript
import path from 'node:path'
|
|
import type { JsonObject } from '../../types/json.js'
|
|
|
|
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
|
import { PROJECT_ROOT } from '../../config/runtime.js'
|
|
import {
|
|
readAppConfigEntry,
|
|
saveAppConfigEntry,
|
|
} from '../config/app-config-store.js'
|
|
|
|
const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json')
|
|
const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health'
|
|
const WORK_ORDER_TIMEOUT_JOB_ID = 'work-order-timeout'
|
|
const DEPOSIT_UNFREEZE_JOB_ID = 'deposit-unfreeze'
|
|
|
|
export function getScheduledJobsFilePath() {
|
|
return SCHEDULED_JOBS_FILE_PATH
|
|
}
|
|
|
|
export function getScheduledJobsConfig() {
|
|
return readAppConfigEntry({
|
|
configKey: APP_CONFIG_KEYS.scheduledJobs,
|
|
legacyFilePath: SCHEDULED_JOBS_FILE_PATH,
|
|
fallback: createDefaultScheduledJobsConfig,
|
|
normalize: normalizeScheduledJobsConfig,
|
|
})
|
|
}
|
|
|
|
export function saveScheduledJobsConfig(rawValue: unknown) {
|
|
return saveAppConfigEntry({
|
|
configKey: APP_CONFIG_KEYS.scheduledJobs,
|
|
legacyFilePath: SCHEDULED_JOBS_FILE_PATH,
|
|
value: rawValue,
|
|
normalize: 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()
|
|
}
|
|
|
|
export function getWorkOrderTimeoutJob(config: JsonObject = getScheduledJobsConfig()) {
|
|
return (Array.isArray(config.jobs) ? config.jobs : [])
|
|
.find((item) => String(item.id || '').trim() === WORK_ORDER_TIMEOUT_JOB_ID)
|
|
|| createDefaultWorkOrderTimeoutJob()
|
|
}
|
|
|
|
export function getDepositUnfreezeJob(config: JsonObject = getScheduledJobsConfig()) {
|
|
return (Array.isArray(config.jobs) ? config.jobs : [])
|
|
.find((item) => String(item.id || '').trim() === DEPOSIT_UNFREEZE_JOB_ID)
|
|
|| createDefaultDepositUnfreezeJob()
|
|
}
|
|
|
|
export function getScheduledJobById(jobId: unknown, config: JsonObject = getScheduledJobsConfig()) {
|
|
return (Array.isArray(config.jobs) ? config.jobs : [])
|
|
.find((item) => String(item.id || '').trim() === String(jobId || '').trim()) || null
|
|
}
|
|
|
|
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 NonNullable<ReturnType<typeof normalizeScheduledJob>> => Boolean(item))
|
|
const hasCloudtentaclesHealth = jobs.some((item) => item.id === CLOUDTENTACLES_HEALTH_JOB_ID)
|
|
const hasWorkOrderTimeout = jobs.some((item) => item.id === WORK_ORDER_TIMEOUT_JOB_ID)
|
|
const hasDepositUnfreeze = jobs.some((item) => item.id === DEPOSIT_UNFREEZE_JOB_ID)
|
|
|
|
if (!hasCloudtentaclesHealth) {
|
|
jobs.push(createDefaultCloudtentaclesHealthJob())
|
|
}
|
|
if (!hasWorkOrderTimeout) {
|
|
jobs.push(createDefaultWorkOrderTimeoutJob())
|
|
}
|
|
if (!hasDepositUnfreeze) {
|
|
jobs.push(createDefaultDepositUnfreezeJob())
|
|
}
|
|
|
|
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 normalizeCloudtentaclesHealthJob(rawValue)
|
|
}
|
|
if (type === 'work_order_timeout') {
|
|
return normalizeWorkOrderTimeoutJob(rawValue)
|
|
}
|
|
if (type === 'deposit_unfreeze') {
|
|
return normalizeDepositUnfreezeJob(rawValue)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function normalizeDepositUnfreezeJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
|
|
return {
|
|
id: DEPOSIT_UNFREEZE_JOB_ID,
|
|
type: 'deposit_unfreeze',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 60, 30, 3600),
|
|
config: {
|
|
scanLimit: normalizeRangeInteger(config.scanLimit, 100, 1, 2000),
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeWorkOrderTimeoutJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
|
|
return {
|
|
id: WORK_ORDER_TIMEOUT_JOB_ID,
|
|
type: 'work_order_timeout',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 60, 30, 3600),
|
|
config: {
|
|
scanLimit: normalizeRangeInteger(config.scanLimit, 50, 1, 1000),
|
|
},
|
|
}
|
|
}
|
|
|
|
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<typeof normalizeCloudtentaclesHealthAccount>[]
|
|
|
|
if (accounts.length === 0) {
|
|
accounts.push(normalizeCloudtentaclesHealthAccount({
|
|
sourceKey: config.sourceKey || 'default',
|
|
assetThreshold: defaultAssetThreshold,
|
|
}, defaultAssetThreshold))
|
|
}
|
|
|
|
const seen = new Set<string>()
|
|
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(),
|
|
createDefaultWorkOrderTimeoutJob(),
|
|
createDefaultDepositUnfreezeJob(),
|
|
],
|
|
}
|
|
}
|
|
|
|
function createDefaultDepositUnfreezeJob() {
|
|
return {
|
|
id: DEPOSIT_UNFREEZE_JOB_ID,
|
|
type: 'deposit_unfreeze',
|
|
enabled: true,
|
|
intervalSeconds: 60,
|
|
config: {
|
|
scanLimit: 100,
|
|
},
|
|
}
|
|
}
|
|
|
|
function createDefaultWorkOrderTimeoutJob() {
|
|
return {
|
|
id: WORK_ORDER_TIMEOUT_JOB_ID,
|
|
type: 'work_order_timeout',
|
|
enabled: true,
|
|
intervalSeconds: 60,
|
|
config: {
|
|
scanLimit: 50,
|
|
},
|
|
}
|
|
}
|
|
|
|
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]'
|
|
}
|