feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"name_comment": "显示的名",
|
||||
"name": "美国战争指数",
|
||||
"card_order": 14,
|
||||
"card_type": "overseas",
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "10m",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "us_war_pizzint",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://fred.stlouisfed.org/series/VIXCLS",
|
||||
"stable_url_comment": "稳定数据源地址(优先)",
|
||||
"stable_url": "https://fred.stlouisfed.org/graph/fredgraph.csv?id=VIXCLS",
|
||||
"pizzint_url_comment": "备用数据源地址(兜底)",
|
||||
"pizzint_url": "https://www.pizzint.watch/",
|
||||
"fail_alert_consecutive_comment": "连续失败达到该次数才触发异常告警",
|
||||
"fail_alert_consecutive": 3,
|
||||
"source_comment": "数据来源",
|
||||
"source": "fred_vix",
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"doughcon_critical_max_comment": "DOUGHCON 小于等于该值触发高风险预警",
|
||||
"doughcon_critical_max": 2,
|
||||
"doughcon_warning_max_comment": "DOUGHCON 小于等于该值触发风险抬升预",
|
||||
"doughcon_warning_max": 3,
|
||||
"vix_warning_min_comment": "VIX 大于等于该值触发风险抬升预",
|
||||
"vix_warning_min": 22,
|
||||
"vix_critical_min_comment": "VIX 大于等于该值触发高风险预警",
|
||||
"vix_critical_min": 30,
|
||||
"index_jump_threshold_comment": "战争指数单次上跳达到该值触发快速升温预",
|
||||
"index_jump_threshold": 20,
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 5,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 120
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": false,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"auth": {
|
||||
"user": "yangdafe@gmail.com",
|
||||
"pass": "{{GMAIL_SMTP_PASS}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": false,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": false,
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": false,
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/calendar/events",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"market_status": {
|
||||
"timezone": "America/New_York",
|
||||
"sessions": [
|
||||
{
|
||||
"start": "00:00",
|
||||
"end": "23:59"
|
||||
}
|
||||
],
|
||||
"display_label": "FRED VIX"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
const fetchText = async (axios, url, headers = {}) => {
|
||||
try {
|
||||
const res = await axios.get(url, {
|
||||
timeout: 10000,
|
||||
validateStatus: () => true,
|
||||
headers
|
||||
})
|
||||
if (!res || res.status >= 400) return null
|
||||
return String(res.data || '')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPizzint = (axios, url) => fetchText(axios, url, {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Referer': 'https://www.pizzint.watch/',
|
||||
'Accept-Language': 'en-US,en;q=0.9'
|
||||
})
|
||||
|
||||
const fetchFredVix = (axios, url) => fetchText(axios, url, {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Accept': 'text/csv,text/plain;q=0.9,*/*;q=0.8'
|
||||
})
|
||||
|
||||
const buildCandidateUrls = (url) => {
|
||||
const primary = String(url || '').trim() || 'https://www.pizzint.watch/'
|
||||
const list = [primary]
|
||||
try {
|
||||
const u = new URL(primary)
|
||||
const host = String(u.hostname || '').toLowerCase()
|
||||
if (host === 'www.pizzint.watch') {
|
||||
list.push('https://pizzint.watch/')
|
||||
} else if (host === 'pizzint.watch') {
|
||||
list.push('https://www.pizzint.watch/')
|
||||
}
|
||||
} catch {}
|
||||
list.push('https://r.jina.ai/http://www.pizzint.watch/')
|
||||
list.push('https://r.jina.ai/http://pizzint.watch/')
|
||||
return [...new Set(list)]
|
||||
}
|
||||
|
||||
const stripHtmlToText = (raw) => String(raw || '')
|
||||
.replace(/<script[\s\S]*?<\/script>/ig, ' ')
|
||||
.replace(/<style[\s\S]*?<\/style>/ig, ' ')
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
const parsePizzint = (raw) => {
|
||||
const html = String(raw || '')
|
||||
const text = stripHtmlToText(html)
|
||||
const doughconMatch =
|
||||
text.match(/DOUGHCON\s*([1-5])/i) ||
|
||||
html.match(/DOUGHCON\s*([1-5])/i) ||
|
||||
text.match(/"doughcon"\s*:\s*([1-5])/i) ||
|
||||
text.match(/"level"\s*:\s*([1-5])/i)
|
||||
if (!doughconMatch) return null
|
||||
const doughcon = Number(doughconMatch[1])
|
||||
if (!Number.isFinite(doughcon)) return null
|
||||
|
||||
const statusMatch =
|
||||
text.match(/STATUS:\s*([A-Z_]+)/i) ||
|
||||
text.match(/\bSTATUS\b\s*:?\s*([A-Z][A-Z _-]{2,24})\b/i) ||
|
||||
html.match(/STATUS:\s*(?:<\/[^>]+>\s*<[^>]+>\s*)*([A-Z][A-Z _-]{2,24})\s*</i) ||
|
||||
text.match(/"status"\s*:\s*"([A-Z_]+)"/i)
|
||||
const status = statusMatch ? String(statusMatch[1]).trim().replace(/\s+/g, '_').toUpperCase() : 'UNKNOWN'
|
||||
|
||||
const locationsMatch =
|
||||
text.match(/(\d+)\s*LOCATIONS?\s*MONITORED/i) ||
|
||||
text.match(/"locationsMonitored"\s*:\s*(\d+)/i) ||
|
||||
text.match(/"locations"\s*:\s*(\d+)/i)
|
||||
const locations = locationsMatch ? Number(locationsMatch[1]) : null
|
||||
|
||||
const timeMatch =
|
||||
text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/) ||
|
||||
text.match(/"timestamp"\s*:\s*"([0-9T:\-.Z]+)"/i)
|
||||
const capturedAt = timeMatch
|
||||
? String(timeMatch[1]).replace('T', ' ').replace('Z', '').replace(/\s+/, ' ')
|
||||
: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||
|
||||
const feedFailed = /CONNECTION FAILED|Data fetch error|ERROR:\s*Failed to fetch|Failed to fetch/i.test(text)
|
||||
const warIndex = Math.max(0, Math.min(100, (6 - doughcon) * 20))
|
||||
|
||||
return { doughcon, status, locations, capturedAt, feedFailed, warIndex }
|
||||
}
|
||||
|
||||
const clamp = (n, min, max) => Math.max(min, Math.min(max, n))
|
||||
|
||||
const vixToWarIndex = (vix) => {
|
||||
const base = ((vix - 12) / 38) * 100
|
||||
return clamp(base, 0, 100)
|
||||
}
|
||||
|
||||
const classifyVixStatus = (vix) => {
|
||||
if (vix >= 30) return 'STRESSED'
|
||||
if (vix >= 22) return 'ELEVATED'
|
||||
return 'CALM'
|
||||
}
|
||||
|
||||
const parseFredVix = (raw) => {
|
||||
const lines = String(raw || '').split(/\r?\n/).map(x => x.trim()).filter(Boolean)
|
||||
if (lines.length < 2) return null
|
||||
const points = []
|
||||
for (let i = 1; i < lines.length; i += 1) {
|
||||
const row = lines[i]
|
||||
const idx = row.indexOf(',')
|
||||
if (idx < 0) continue
|
||||
const date = row.slice(0, idx).trim()
|
||||
const valueRaw = row.slice(idx + 1).trim()
|
||||
const value = Number(valueRaw)
|
||||
if (!date || !Number.isFinite(value)) continue
|
||||
points.push({ date, value })
|
||||
}
|
||||
if (!points.length) return null
|
||||
const last = points[points.length - 1]
|
||||
const prev = points.length > 1 ? points[points.length - 2] : null
|
||||
const warIndex = vixToWarIndex(last.value)
|
||||
const prevWarIndex = prev ? vixToWarIndex(prev.value) : warIndex
|
||||
return {
|
||||
provider: 'fred_vix',
|
||||
vix: last.value,
|
||||
prevVix: prev ? prev.value : last.value,
|
||||
warIndex,
|
||||
prevWarIndex,
|
||||
status: classifyVixStatus(last.value),
|
||||
capturedAt: `${last.date} 00:00:00`
|
||||
}
|
||||
}
|
||||
|
||||
const getWarRiskBand = (warIndex) => {
|
||||
if (!Number.isFinite(warIndex)) return '未知'
|
||||
if (warIndex < 30) return '低风险'
|
||||
if (warIndex < 60) return '中风险'
|
||||
return '高风险'
|
||||
}
|
||||
|
||||
const buildExplainText = (snapshot, sourceLabel) => {
|
||||
const warIndex = Number(snapshot && snapshot.warIndex)
|
||||
const band = getWarRiskBand(warIndex)
|
||||
if (snapshot && snapshot.provider === 'fred_vix') {
|
||||
const vix = Number(snapshot.vix)
|
||||
return `指数说明:0-100 越高风险越高;当前 ${warIndex.toFixed(2)}(${band})。数据源 ${sourceLabel},VIX ${vix.toFixed(2)} 代表市场波动预期。`
|
||||
}
|
||||
const doughcon = Number(snapshot && snapshot.doughcon)
|
||||
const status = String((snapshot && snapshot.status) || 'UNKNOWN')
|
||||
return `指数说明:0-100 越高风险越高;当前 ${warIndex.toFixed(2)}(${band})。数据源 ${sourceLabel},DOUGHCON ${doughcon} / 状态 ${status}。`
|
||||
}
|
||||
|
||||
const buildAlerts = (snapshot, prevIndex, config) => {
|
||||
const alerts = []
|
||||
const cfg = config && config.alerts ? config.alerts : {}
|
||||
const jumpThreshold = Number.isFinite(cfg.index_jump_threshold) ? cfg.index_jump_threshold : 20
|
||||
const warIndex = snapshot.warIndex
|
||||
const delta = Number.isFinite(prevIndex) ? warIndex - prevIndex : 0
|
||||
|
||||
if (snapshot.provider === 'fred_vix') {
|
||||
const vixWarningMin = Number.isFinite(cfg.vix_warning_min) ? cfg.vix_warning_min : 22
|
||||
const vixCriticalMin = Number.isFinite(cfg.vix_critical_min) ? cfg.vix_critical_min : 30
|
||||
if (snapshot.vix >= vixCriticalMin) {
|
||||
alerts.push({
|
||||
id: `vix_critical_${Math.floor(snapshot.vix)}`,
|
||||
title: `${config.name}高风险预警`,
|
||||
message: `VIX ${snapshot.vix.toFixed(2)},战争指数 ${warIndex.toFixed(2)},进入高警戒区`,
|
||||
level: 'critical'
|
||||
})
|
||||
} else if (snapshot.vix >= vixWarningMin) {
|
||||
alerts.push({
|
||||
id: `vix_warning_${Math.floor(snapshot.vix)}`,
|
||||
title: `${config.name}风险抬升预警`,
|
||||
message: `VIX ${snapshot.vix.toFixed(2)},战争指数 ${warIndex.toFixed(2)},建议关注地缘冲突动态`,
|
||||
level: 'warning'
|
||||
})
|
||||
}
|
||||
if (Number.isFinite(prevIndex) && delta >= jumpThreshold) {
|
||||
alerts.push({
|
||||
id: `jump_${Math.floor(delta)}`,
|
||||
title: `${config.name}快速升温预警`,
|
||||
message: `指数单次上跳 ${delta.toFixed(2)},当前 ${warIndex.toFixed(2)},上一值 ${prevIndex.toFixed(2)}`,
|
||||
level: 'critical'
|
||||
})
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
const criticalMax = Number.isFinite(cfg.doughcon_critical_max) ? cfg.doughcon_critical_max : 2
|
||||
const warningMax = Number.isFinite(cfg.doughcon_warning_max) ? cfg.doughcon_warning_max : 3
|
||||
const doughcon = snapshot.doughcon
|
||||
const status = snapshot.status
|
||||
if (snapshot.feedFailed) {
|
||||
alerts.push({
|
||||
id: 'feed_failed',
|
||||
title: `${config.name}数据源异常`,
|
||||
message: `PizzINT 页面出现数据抓取失败信号,请人工复核。当前 DOUGHCON ${doughcon}`,
|
||||
level: 'warning'
|
||||
})
|
||||
}
|
||||
if (status && status !== 'OPERATIONAL' && status !== 'UNKNOWN') {
|
||||
alerts.push({
|
||||
id: `status_${status.toLowerCase()}`,
|
||||
title: `${config.name}运行状态异常`,
|
||||
message: `当前状态 ${status},DOUGHCON ${doughcon},指数 ${warIndex.toFixed(2)}`,
|
||||
level: 'warning'
|
||||
})
|
||||
}
|
||||
if (doughcon <= criticalMax) {
|
||||
alerts.push({
|
||||
id: `doughcon_critical_${doughcon}`,
|
||||
title: `${config.name}高风险预警`,
|
||||
message: `DOUGHCON ${doughcon},战争指数 ${warIndex.toFixed(2)},进入高警戒区`,
|
||||
level: 'critical'
|
||||
})
|
||||
} else if (doughcon <= warningMax) {
|
||||
alerts.push({
|
||||
id: `doughcon_warning_${doughcon}`,
|
||||
title: `${config.name}风险抬升预警`,
|
||||
message: `DOUGHCON ${doughcon},战争指数 ${warIndex.toFixed(2)},建议关注地缘冲突动态`,
|
||||
level: 'warning'
|
||||
})
|
||||
}
|
||||
if (Number.isFinite(prevIndex) && delta >= jumpThreshold) {
|
||||
alerts.push({
|
||||
id: `jump_${Math.floor(delta)}`,
|
||||
title: `${config.name}快速升温预警`,
|
||||
message: `指数单次上跳 ${delta.toFixed(2)},当前 ${warIndex.toFixed(2)},上一值 ${prevIndex.toFixed(2)}`,
|
||||
level: 'critical'
|
||||
})
|
||||
}
|
||||
|
||||
return alerts
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
run: async (context, config) => {
|
||||
const { axios, db, logJSON } = context
|
||||
const stableUrl = config.stable_url || 'https://fred.stlouisfed.org/graph/fredgraph.csv?id=VIXCLS'
|
||||
const pizzintUrl = config.pizzint_url || 'https://www.pizzint.watch/'
|
||||
const stateKey = `skill_state:${String(config.symbol || 'us_war_pizzint')}:war_index`
|
||||
const failCountKey = `${stateKey}:fail_count`
|
||||
const failAlertThreshold = Number.isFinite(Number(config && config.fail_alert_consecutive))
|
||||
? Math.max(1, Number(config.fail_alert_consecutive))
|
||||
: 3
|
||||
const getState = db.prepare(`SELECT value FROM market_state WHERE key = ?`)
|
||||
const upsertState = db.prepare(
|
||||
`INSERT INTO market_state (key, value, updated_at)
|
||||
VALUES (@key, @value, datetime('now'))
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = datetime('now')`
|
||||
)
|
||||
const stateRow = getState.get(stateKey)
|
||||
const failCountRow = getState.get(failCountKey)
|
||||
const prevIndex = stateRow && Number.isFinite(Number(stateRow.value)) ? Number(stateRow.value) : NaN
|
||||
const failCount = failCountRow && Number.isFinite(Number(failCountRow.value)) ? Math.max(0, Number(failCountRow.value)) : 0
|
||||
const fallbackPrice = Number.isFinite(prevIndex) ? prevIndex : 20
|
||||
const buildFallback = (status, message) => {
|
||||
const nextFailCount = failCount + 1
|
||||
upsertState.run({ key: failCountKey, value: String(nextFailCount) })
|
||||
const explain = `指数说明:0-100 越高风险越高;当前为兜底值 ${fallbackPrice.toFixed(2)}。稳定源与备用源连续失败时会回退。`
|
||||
const data = [{
|
||||
label: config.name,
|
||||
symbol: config.symbol,
|
||||
source: 'war_index_fallback',
|
||||
name: `DOUGHCON N/A · ${status}`,
|
||||
price: fallbackPrice,
|
||||
prevClose: fallbackPrice,
|
||||
change: 0,
|
||||
change_percent: 0,
|
||||
captured_at: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
||||
market_status: config.market_status,
|
||||
pizzint_doughcon: null,
|
||||
pizzint_status: status,
|
||||
pizzint_locations: null,
|
||||
pizzint_feed_failed: 1,
|
||||
explain
|
||||
}]
|
||||
const alerts = nextFailCount >= failAlertThreshold
|
||||
? [{
|
||||
id: status.toLowerCase(),
|
||||
title: `${config.name}数据异常`,
|
||||
message: `${message}(连续失败 ${nextFailCount} 次)。${explain}`,
|
||||
level: 'warning'
|
||||
}]
|
||||
: []
|
||||
return { data, alerts }
|
||||
}
|
||||
|
||||
const stableRaw = await fetchFredVix(axios, stableUrl)
|
||||
const stableSnapshot = stableRaw ? parseFredVix(stableRaw) : null
|
||||
if (stableSnapshot) {
|
||||
if (failCount > 0) upsertState.run({ key: failCountKey, value: '0' })
|
||||
const warIndex = stableSnapshot.warIndex
|
||||
const prevClose = Number.isFinite(prevIndex) ? prevIndex : stableSnapshot.prevWarIndex
|
||||
const change = warIndex - prevClose
|
||||
const changePercent = prevClose ? (change / prevClose) * 100 : 0
|
||||
const explain = buildExplainText(stableSnapshot, 'FRED VIX')
|
||||
upsertState.run({ key: stateKey, value: String(warIndex) })
|
||||
const data = [{
|
||||
label: config.name,
|
||||
symbol: config.symbol,
|
||||
source: 'fred_vix',
|
||||
name: `VIX ${stableSnapshot.vix.toFixed(2)} · ${stableSnapshot.status}`,
|
||||
price: warIndex,
|
||||
prevClose,
|
||||
change,
|
||||
change_percent: changePercent,
|
||||
captured_at: stableSnapshot.capturedAt,
|
||||
market_status: config.market_status,
|
||||
pizzint_doughcon: null,
|
||||
pizzint_status: null,
|
||||
pizzint_locations: null,
|
||||
pizzint_feed_failed: 0,
|
||||
explain
|
||||
}]
|
||||
const alerts = buildAlerts(stableSnapshot, Number.isFinite(prevIndex) ? prevIndex : null, config)
|
||||
return { data, alerts }
|
||||
}
|
||||
|
||||
const candidates = buildCandidateUrls(pizzintUrl)
|
||||
let snapshot = null
|
||||
let fetchedCount = 0
|
||||
let parseFailedCount = 0
|
||||
let resolvedFrom = ''
|
||||
for (const candidate of candidates) {
|
||||
const html = await fetchPizzint(axios, candidate)
|
||||
if (!html) continue
|
||||
fetchedCount += 1
|
||||
const parsed = parsePizzint(html)
|
||||
if (parsed) {
|
||||
snapshot = parsed
|
||||
resolvedFrom = candidate
|
||||
break
|
||||
}
|
||||
parseFailedCount += 1
|
||||
}
|
||||
|
||||
if (!snapshot && fetchedCount === 0) {
|
||||
logJSON('skill.fetch.error', { skill: config.name, message: 'All sources fetch failed', candidates: candidates.length, stableUrl }, 'market_skills')
|
||||
return buildFallback('FETCH_FAILED', '稳定源 FRED VIX 与备用源 PizzINT 均抓取失败,已回退到兜底数值')
|
||||
}
|
||||
if (!snapshot) {
|
||||
logJSON('skill.parse.error', { skill: config.name, message: 'PizzINT parse failed after stable source failure', fetchedCount, parseFailedCount, stableUrl }, 'market_skills')
|
||||
return buildFallback('PARSE_FAILED', '稳定源不可用且备用源解析失败,已回退到兜底数值')
|
||||
}
|
||||
if (resolvedFrom && String(resolvedFrom) !== String(pizzintUrl)) {
|
||||
logJSON('skill.fetch.fallback', { skill: config.name, resolvedFrom }, 'market_skills')
|
||||
}
|
||||
if (failCount > 0) upsertState.run({ key: failCountKey, value: '0' })
|
||||
const warIndex = snapshot.warIndex
|
||||
const prevClose = Number.isFinite(prevIndex) ? prevIndex : warIndex
|
||||
const change = warIndex - prevClose
|
||||
const changePercent = prevClose ? (change / prevClose) * 100 : 0
|
||||
const explain = buildExplainText(snapshot, 'PizzINT')
|
||||
|
||||
upsertState.run({ key: stateKey, value: String(warIndex) })
|
||||
|
||||
const data = [{
|
||||
label: config.name,
|
||||
symbol: config.symbol,
|
||||
source: 'pizzint',
|
||||
name: `DOUGHCON ${snapshot.doughcon} · ${snapshot.status}`,
|
||||
price: warIndex,
|
||||
prevClose,
|
||||
change,
|
||||
change_percent: changePercent,
|
||||
captured_at: snapshot.capturedAt,
|
||||
market_status: config.market_status,
|
||||
pizzint_doughcon: snapshot.doughcon,
|
||||
pizzint_status: snapshot.status,
|
||||
pizzint_locations: snapshot.locations,
|
||||
pizzint_feed_failed: snapshot.feedFailed ? 1 : 0,
|
||||
explain
|
||||
}]
|
||||
|
||||
const alerts = buildAlerts(snapshot, Number.isFinite(prevIndex) ? prevIndex : null, config)
|
||||
return { data, alerts }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user